diff --git a/README.md b/README.md index 81f885e8fee..034006d9c93 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ both for guidelines on code formatting and git usage. And [AngelScript primer][asprimer] for scripting help. ### C++ Programmers -To compile Thrive yourself, you will not only need to follow the [setup instructions][setupguide]. +To compile Thrive yourself, you will need to follow the [setup instructions][setupguide]. Be sure to have a look at the [styleguide][styleguide], both for guidelines on code formatting and git usage. diff --git a/SetupThrive.rb b/SetupThrive.rb index 59809a5bd6d..30270d2fe5d 100755 --- a/SetupThrive.rb +++ b/SetupThrive.rb @@ -78,7 +78,7 @@ def parseExtraArgs leviathan = Leviathan.new( # Use this if you always want the latest commit # version: "develop", - version: "55f85074961175d8ffecfca2e318eb093b2d9c68", + version: "28cd2c52dd583e9537f1df32cac91d9b4ed2da2d", # Doesn't actually work, but leviathan doesn't install with sudo by # default, or install at all for that matter noInstallSudo: true diff --git a/doc/setup_instructions.md b/doc/setup_instructions.md index 899e27e7644..42791245f51 100644 --- a/doc/setup_instructions.md +++ b/doc/setup_instructions.md @@ -1,7 +1,7 @@ What's this? ============ -This is the setup instructions for compiling Thrive. +These are the setup instructions for compiling Thrive. Important Note: If you run into any trouble with the setup scripts, please bring them up on the development discord or open a github issue. @@ -174,7 +174,8 @@ graphical tool for reviewing the exact lines you have changed, but you can also commit on the command line) you can publish them to your fork with (assuming you used the master branch, when working with the main thrive repository you MUST create a different branch but when working -with a fork that isn't required): +with a fork that isn't required, but still strongly recommended as +making multiple pull requests with one branch is messy): ``` git push fork master diff --git a/doc/style_guide.md b/doc/style_guide.md index fbb966a13a4..e1d50a208f0 100644 --- a/doc/style_guide.md +++ b/doc/style_guide.md @@ -1,14 +1,14 @@ Code Style Guide ================ -To maintain a consistent coding style, contributors should follow the rules -outlined on this page. This style guide is separated into four parts: common +To maintain a consistent coding style, contributors should follow the rules +outlined on this page. This style guide is separated into four parts: common rules, rules specific for C++, rules specific for AngelScript and guidelines for using git. -The style rules are intended to increase readability of the source code. The -most important rule of all is: **Use common sense**. If you have to break -some rules to make the code more readable (and not just for you, but for +The style rules are intended to increase readability of the source code. The +most important rule of all is: **Use common sense**. If you have to break +some rules to make the code more readable (and not just for you, but for everyone who has to read your code), break it. Common (Both C++ and AngelScript) @@ -17,11 +17,11 @@ Common (Both C++ and AngelScript) - Indentation is 4 spaces - Names (that includes variables, functions and classes) should be descriptive. - Avoid abbreviations. Do not shorten variable names just to save key strokes, + Avoid abbreviations. Do not shorten variable names just to save key strokes, it will be read far more often than it will be written. -- Variables and functions are camelCase with leading lower case. Classes are - CamelCase with leading upper case. Constants are CONSTANT_CASE with +- Variables and functions are camelCase with leading lower case. Classes are + CamelCase with leading upper case. Constants are CONSTANT_CASE with underscores. - Filenames are lower_case with underscores. The reason for this is @@ -34,26 +34,25 @@ Common (Both C++ and AngelScript) C++ --- +- Format your code with [clang-format](clang_format.md) this is the + official formatting and most important thing to consider. If you + don't automatic checks on your code will fail. + - Macros are CONSTANT_CASE - Header files end in .h, source files in .cpp -- Header files should begin with `#pragma once`. Old-style header +- Header files should begin with `#pragma once`. Old-style header guards (with `ifdef`) are discouraged because they are very verbose and the pragma is understood by all relevant compilers. - -- Format your code with [clang-format](clang_format.md) - -- Opening braces go in the same line as the control statement, closing braces - are aligned below the first character of the opening control statement - Keep header files minimal. Ideally, they show only functions / classes that are useful to other code. All internal helper functions / classes should be declared and defined in the source file. - All classes and their public and protected members should be documented by - doxygen comments in the header file. If the function's purpose is clear - from the name and its parameters (which should be the usual case), the + doxygen comments in the header file. If the function's purpose is clear + from the name and its parameters (which should be the usual case), the comment can be very basic and only serves to shut up doxygen warnings about undocumented stuff. @@ -61,19 +60,21 @@ C++ you wrote the function like this (and, sometimes more importantly, why you didn't go another way). -- Member variables of classes are prefixed with \p m_. This is to - differentiate them from local or global variables when using their +- Empty lines are encouraged between blocks of code to improve readability. + +- Member variables of classes are prefixed with \p m_. This is to + differentiate them from local or global variables when using their unqualified name (without `this->`) inside member functions. The prefix can be omitted for very simple structs if they don't have member functions and serve only as data container. -(- When calling member functions from another member function, their names are - qualified with `this->` to differentiate them from global non-member - functions.) +- When calling member functions from another member function, their names may be + qualified with `this->` to differentiate them from global non-member + functions. This rule is not enforced. -- Function signatures are formatted like the clang-format options file makes them to be formatted - -- For non-trivial classes that would pull in a lot of other headers, use the pimpl idiom to hide implem entation details and only include the ton of headers in the .cpp file. +- For non-trivial classes that would pull in a lot of other headers, + use the pimpl idiom to hide implementation details and only include + the ton of headers in the .cpp file. ```cpp // In the header: @@ -92,12 +93,12 @@ C++ ~MyClass(); private: - + struct Implementation; std::unique_ptr m_impl; }; ``` - + ```cpp // In the source file: @@ -112,24 +113,24 @@ C++ MyClass::~MyClass() {} // Define destructor ``` - -- Try to avoid include statements inside header files unless - absolutely necessary. Prefer forward declarations and put the - include inside the source file instead. And use the pimpl idiom if - this cannot be avoided and the headers are large. -- Prefer C++11's `using` over `typedef`. With the `using` keyword, type +- Try to avoid include statements inside header files. Prefer forward + declarations and put the include inside the source file instead. And + use the pimpl idiom if this cannot be avoided and the headers are + large. + +- Use C++11's `using` over `typedef`. With the `using` keyword, type aliases look more like familiar variable assignment, with no ambiguity as to which is the newly defined type name. -- Virtual member functions overridden in derived classes are marked with the - C++11 `override` keyword. This will (correctly) cause a compile time error - when the function signature in the base class changes and the programmer +- Virtual member functions overridden in derived classes are marked with the + C++11 `override` keyword. This will (correctly) cause a compile time error + when the function signature in the base class changes and the programmer forgot to update the derived class. - Classes not intended as base classes are marked with the `final` keyword like this: - + ```cpp class MyClass final { // ... @@ -147,7 +148,7 @@ C++ #include #include - + #include #include @@ -160,18 +161,16 @@ C++ AngelScript ----------- -- A class's public data members are *not* prefixed by `m_`, unlike C++. - -(This is because in AngelScript, all member variables are accessed with their qualified - names (like `this.memberVariable`), so there is no need to mark them.) +- A class's public data members don't need to be prefixed by `m_`, unlike C++. - A class's private data members and functions are declared `private` (everything is public by default) and optionally prefixed with an underscore. This is a convention adopted from Python's PEP8 style - guide. + guide. This same rule may also be used in C++. -- Doxygen does not support AngelScript natively, but for consistency's sake, AngelScript - classes and functions are still documented with doxygen style comments. +- Doxygen does not support AngelScript natively, but for consistency's + sake, AngelScript classes and functions are still documented with + doxygen style comments. - For consistency with C++ adding semicolons after class declarations is recommended, but not an error if omitted. This is one of the @@ -181,9 +180,11 @@ AngelScript Git --- -- Do not work in the master branch, always create a private feature branch - if you want to edit something - +- Do not work in the master branch, always create a private feature + branch if you want to edit something. Even when working with a fork + this is recommended to reduce problems with subsequent pull + requests. + - If you don't have access to the main repository yet (you will be granted access after your first accepted pull request) fork the Thrive repository and work in your fork and once done open a pull @@ -204,7 +205,10 @@ Git - When the master branch is updated, you should usually keep your feature branch as-is. If you really need the new features from master, do a merge. Or if there is a merge conflict preventing your - pull request from being merged. + pull request from being merged. Quite often the master branch needs + to be merged in before merging a pull request. For this it is + cleaner if the pull request is rebased onto master, but this is not + required. - When a feature branch is done, open a pull request on GitHub so that others can review it. Chances are that during this review, there will still be @@ -215,13 +219,16 @@ Git done (take note people accepting pull requests) from the Github accept pull request button, hit the arrow to the right side and select "merge and squash". Bigger feature branches that are dozens - of commits from multiple people will be merged normally to attribute - all of the authors on Github feeds. - -- For maintainers: When manually squashing GitHub (which is something - you should avoid) requires a merge commit to recognize the merging - of a pull request. A "git merge --squash" does not create a merge - commit and will leave the pull request's branch "dangling". To make - GitHub properly reflect the merge, follow the procedure outlined in - the previous bullet point, then click the "Merge Pull Request" - button on GitHub (or do a normal "git merge". + of commits or from multiple people need to be manually squashed into + a few commits while keeping commits from different authors separate + in order to attribute all of the authors on Github feeds. These kind + of pull requests can be merged normally **if** all the commits in them + are "clean" to not dirty up master. + +- For maintainers: When manually merging (which is something you + should avoid) GitHub requires a merge commit to recognize the + merging of a pull request. A "git merge --squash" does not create a + merge commit and will leave the pull request's branch "dangling". To + make GitHub properly reflect the merge, follow the procedure + outlined in the previous bullet point, then click the "Merge Pull + Request" button on GitHub (or do a normal "git merge"). diff --git a/scripts/SimulationParameters/MicrobeStage/BioProcesses.json b/scripts/SimulationParameters/MicrobeStage/BioProcesses.json index 2bea03ce98e..e0359718b7f 100644 --- a/scripts/SimulationParameters/MicrobeStage/BioProcesses.json +++ b/scripts/SimulationParameters/MicrobeStage/BioProcesses.json @@ -79,11 +79,12 @@ "name": "Nitrogen Fixing", "inputs": { - "atp": 0.5 + "oxygen": 1.0, + "atp": 5 }, "outputs": { - "ammonia": 0.1 + "ammonia": 0.5 } }, @@ -110,5 +111,18 @@ "outputs": { "glucose": 0.33 } + }, + + "iron_chemolithoautotrophy": { + "name": "Iron Chemolithoautotrophy", + + "inputs": { + "carbondioxide": 0.09, + "iron": 0.175 + }, + + "outputs": { + "atp": 10.0 + } } } diff --git a/scripts/SimulationParameters/MicrobeStage/Biomes.json b/scripts/SimulationParameters/MicrobeStage/Biomes.json index 2ed652b12d2..83e6a70f4e5 100644 --- a/scripts/SimulationParameters/MicrobeStage/Biomes.json +++ b/scripts/SimulationParameters/MicrobeStage/Biomes.json @@ -154,7 +154,7 @@ "density": 0.00002, "dissolved": 0.0 }, - + "oxygen": { "amount": 0, "density": 0.0, @@ -208,7 +208,7 @@ "density": 0.00002, "dissolved": 0.0 }, - + "oxygen": { "amount": 0, "density": 0.0, @@ -430,7 +430,7 @@ "density": 0.00002, "dissolved": 0.0 }, - + "oxygen": { "amount": 0, "density": 0.0, diff --git a/scripts/SimulationParameters/MicrobeStage/Compounds.json b/scripts/SimulationParameters/MicrobeStage/Compounds.json index c0979c86914..4cc5fc50dcd 100644 --- a/scripts/SimulationParameters/MicrobeStage/Compounds.json +++ b/scripts/SimulationParameters/MicrobeStage/Compounds.json @@ -19,9 +19,9 @@ "isUseful": true, "isEnvironmental": false, "colour": { - "r": 0.6, - "g": 0.7, - "b": 0.2 + "r": 1.0, + "g": 0.4, + "b": 0.1 } }, @@ -45,9 +45,9 @@ "isUseful": false, "isEnvironmental": false, "colour": { - "r": 1.0, - "g": 0.4, - "b": 0.1 + "r": 0.6, + "g": 0.7, + "b": 0.2 } }, @@ -77,6 +77,19 @@ } }, + "iron": { + "name": "Iron", + "volume": 1, + "isCloud": true, + "isUseful": false, + "isEnvironmental": false, + "colour": { + "r": 0.28, + "g": 0.098, + "b": 0.02 + } + }, + "oxygen": { "name": "Oxygen", "volume": 1, diff --git a/scripts/gui/microbe_editor.mjs b/scripts/gui/microbe_editor.mjs index 408bc803cfc..d486079e533 100644 --- a/scripts/gui/microbe_editor.mjs +++ b/scripts/gui/microbe_editor.mjs @@ -52,6 +52,10 @@ const organelleSelectionElements = [ element: document.getElementById("addChemoSynthisizingProteins"), organelle: "chemoSynthisizingProteins" }, + { + element: document.getElementById("addRusticyanin"), + organelle: "rusticyanin" + }, { element: document.getElementById("addFlagellum"), organelle: "flagellum" diff --git a/scripts/gui/microbe_hud.mjs b/scripts/gui/microbe_hud.mjs index 3e159265140..b98d879442e 100644 --- a/scripts/gui/microbe_hud.mjs +++ b/scripts/gui/microbe_hud.mjs @@ -91,6 +91,9 @@ export function runMicrobeHUDSetup(){ // Event that enables the editor button Leviathan.OnGeneric("PlayerReadyToEnterEditor", onReadyToEnterEditor); + // Event that disabled the editor button + Leviathan.OnGeneric("PlayerDiedBeforeEnter", onResetEditor); + // Add listner for sucide button document.getElementById("suicideButton").addEventListener("click", killPlayerCell, true); @@ -104,6 +107,7 @@ export function runMicrobeHUDSetup(){ const oxytoxy = common.randomBetween(0, 10); const phosphate = common.randomBetween(0, 50); const hydrogenSulfide = common.randomBetween(0, 50); + const iron = common.randomBetween(0, 50); updateMicrobeHUDBars({ hitpoints: common.randomBetween(1, hp), maxHitpoints: hp, @@ -119,6 +123,8 @@ export function runMicrobeHUDSetup(){ PhosphateMax: phosphate, compoundHydrogenSulfide: common.randomBetween(0, hydrogenSulfide), HydrogenSulfideMax: hydrogenSulfide, + compoundIron: common.randomBetween(0, iron), + IronMax: iron, }); // Pseudo population code @@ -152,6 +158,15 @@ export function onReadyToEnterEditor(){ } +//! Disabled the editor button +export function onResetEditor(){ + + // Disable + document.getElementById("microbeToEditorButton").classList.add("DisabledButton"); + readyToEdit = false; +} + + function onCompoundPanelClicked() { common.playButtonPressSound(); @@ -425,7 +440,7 @@ function checkGeneration (generation, population){ document.getElementById("winBody").style.display = "inline-block"; document.getElementById("winContainer").style.display = "inline-block"; wonOnce = true; - setTimeout(hideWinText, 7000); + setTimeout(hideWinText, 14000); } } @@ -493,4 +508,11 @@ function updateMicrobeHUDBars(values){ document.getElementById("microbeHUDPlayerHydrogenSulfideBar").style.width = common.barHelper(values.compoundHydrogenSulfide, values.HydrogenSulfideMax); + document.getElementById("microbeHUDPlayerIron").textContent = + values.compoundIron.toFixed(1); + document.getElementById("microbeHUDPlayerIronMax").textContent = + values.IronMax; + document.getElementById("microbeHUDPlayerIronBar").style.width = + common.barHelper(values.compoundIron, values.IronMax); + } diff --git a/scripts/gui/thrive_gui.html b/scripts/gui/thrive_gui.html index 615fbfe7992..eb60291125b 100644 --- a/scripts/gui/thrive_gui.html +++ b/scripts/gui/thrive_gui.html @@ -251,7 +251,20 @@ - + + +
+
+
+
+
Iron Ions
+
+ 0 / + 0 +
+
+
+
@@ -392,6 +405,26 @@ sulfide from hydrothermal vents into usable energy in the form of glucose.
Chemosynthisizing Proteins
45 MP + + + +
+ + Chromatophores

Cost: 25 mutation points

+ Performs Process: Chromatophore Photosynthesis
(0.09 CO2 -> 0.33 glucose)/Second (Depending On Environmental C02)
+ Performs Process: Glycolysis
(0.125 glucose -> 5 ATP)/Second (Depending On Environmental C02)

+ Storage Space: 10

+ Coloured, membrane-associated vesicles used by various prokaryotes perform photosynthesis. + Chromatophores contain bacteriochlorophyll pigments and carotenoids.
+
Chromatophores
25 MP
+ + Rusticyanin

Cost: 45 mutation points

+ Performs Process: Iron Chemolithotrophy
(0.09 CO2 + 0.5 Iron Ion -> 10 ATP)/Second (Depending On Environmental C02)
+ Storage Space: 5

+ Siderophores and Rusticyanin for storing and using iron ions and carbon from atmospheric carbon dioxide to produce ATP. + Iron Chemolithotrophy is a process by which organisms obtain their energy from the oxidation of reduced inorganic ions. +
+
Rusticyanin
45 MP
EXTERNAL ORGANELLES
+
Cilia
40 MP
@@ -403,7 +436,7 @@
Predatory Pilus
30 MP
-
Cilia
40 MP
INTERNAL ORGANELLES
@@ -452,12 +485,12 @@
Chemoplast
45 MP + Nitrogen Fixing
Plastid
50 MP - +
- Nitrogen Fixing Plastid

Cost: 80 mutation points

- Performs Process: Nitrogen Fixation
(0.5 ATP -> 0.1 Ammonia)/Second

- Allows for synthesis of ammonia from atmospheric nitrogen. For easier cell growth. + Nitrogen Fixing Plastid

Cost: 50 mutation points

+ Performs Process: Nitrogen Fixation
(1 Oxygen + 5 ATP -> 0.5 Ammonia)/Second (Depending On Environmental Oxygen)

+ Allows for synthesis of ammonia from atmospheric nitrogen and oxygen. For easier cell growth.
-
Nitrogen Fixing Plastid
80 MP
@@ -472,8 +505,8 @@ Allows for production and storage of OxytoxyNT which can be shot at enemy cells using E. The more of this organelle you have the faster your toxin fire rate aswell.
Toxin Vacuole
70 MP
-
Bioluminescent Vacuole
N/A MP
+
Bioluminescent Vacuole
N/A MP
diff --git a/scripts/gui/thrive_style.css b/scripts/gui/thrive_style.css index be1a60e5c95..1a76af88e4a 100644 --- a/scripts/gui/thrive_style.css +++ b/scripts/gui/thrive_style.css @@ -476,6 +476,14 @@ video { background-color: #a02355; } +#IronIcon { + background-image: url("../../Textures/gui/bevel/Iron.png"); +} + +#microbeHUDPlayerIronBar { + background-color: #b7410e; +} + #HealthIcon { background-image: url("../../Textures/gui/bevel/Hitpoints.png"); } @@ -1258,6 +1266,15 @@ video { background-image: url("../../Textures/gui/bevel/ChemoproteinsIcon.png"); } +#RusticyaninIcon { + position: relative; + left: calc(50% - 30px); + width: 60px; + height: 60px; + background-size: contain; + background-image: url("../../Textures/gui/bevel/ChemoproteinsIcon.png"); +} + #PilusIcon { position: relative; left: calc(50% - 30px); diff --git a/scripts/microbe_stage/biome.as b/scripts/microbe_stage/biome.as index 2c57b47f0d6..2f3611cb64a 100644 --- a/scripts/microbe_stage/biome.as +++ b/scripts/microbe_stage/biome.as @@ -56,13 +56,12 @@ void setBiome(uint64 biomeId, CellStageWorld@ world){ LOG_INFO("Setting biome to: " + biomeId); // Getting the base biome to change to. currentBiome = biomeId; - auto biome = getCurrentBiome(); auto biomeCompounds = biome.getCompoundKeys(); LOG_INFO("biomeCompounds.length = " + biomeCompounds.length()); for(uint i = 0; i < biomeCompounds.length(); ++i){ - auto compoundId = biomeCompounds[i]; + auto compoundId = SimulationParameters::compoundRegistry().getTypeData(biomeCompounds[i]).id; if(SimulationParameters::compoundRegistry().getTypeData(compoundId).isCloud){ diff --git a/scripts/microbe_stage/configs.as b/scripts/microbe_stage/configs.as index 0703beaea50..f7779579876 100644 --- a/scripts/microbe_stage/configs.as +++ b/scripts/microbe_stage/configs.as @@ -20,7 +20,7 @@ const auto MAX_COLOR = 0.9f; const auto MIN_COLOR_MUTATION = -0.005f; const auto MAX_COLOR_MUTATION = 0.005f; -const auto MIN_OPACITY = 0.2f; +const auto MIN_OPACITY = 0.5f; const auto MAX_OPACITY = 1.8f; const auto MIN_OPACITY_CHITIN = 0.4f; @@ -160,6 +160,11 @@ const float OXY_TOXY_DAMAGE = 10.0f; // Cooldown between agent emissions, in milliseconds. const uint AGENT_EMISSION_COOLDOWN = 2000; +// Iron amounts per chunk. +// big iron ejects ten per 20 clicks , so about 30 per second, so ill give it enough for 1000 seconds) +const double IRON_PER_BIG_CHUNK = 30000.0f; +// small iron ejects 3 per 20 clicks , so about 9 per second, so ill give it enough for 1000 seconds aswell +const double IRON_PER_SMALL_CHUNK = 9000.0f; //Auto Evo Values const int CREATURE_DEATH_POPULATION_LOSS = -60; const int CREATURE_KILL_POPULATION_GAIN = 50; @@ -344,7 +349,8 @@ const dictionary STARTER_MICROBES = { {"ammonia", InitialCompound(0)}, {"phosphates", InitialCompound(0)}, {"hydrogensulfide", InitialCompound(0)}, - {"oxytoxy", InitialCompound(0)} + {"oxytoxy", InitialCompound(0)}, + {"iron", InitialCompound(3)} }, { OrganelleTemplatePlaced("cytoplasm", 0, 0, 0) diff --git a/scripts/microbe_stage/microbe_editor/microbe_editor.as b/scripts/microbe_stage/microbe_editor/microbe_editor.as index a60c6007f10..76980186888 100644 --- a/scripts/microbe_stage/microbe_editor/microbe_editor.as +++ b/scripts/microbe_stage/microbe_editor/microbe_editor.as @@ -63,6 +63,7 @@ class MicrobeEditor{ {"chromatophors", PlacementFunctionType(this.addOrganelle)}, {"metabolosome", PlacementFunctionType(this.addOrganelle)}, {"chemoSynthisizingProteins", PlacementFunctionType(this.addOrganelle)}, + {"rusticyanin", PlacementFunctionType(this.addOrganelle)}, {"remove", PlacementFunctionType(this.removeOrganelle)} }; } @@ -70,7 +71,6 @@ class MicrobeEditor{ //! This is called each time the editor is entered so this needs to properly reset state void init() { - updateGuiButtonStatus(checkIsNucleusPresent()); gridSceneNode = hudSystem.world.CreateEntity(); auto node = hudSystem.world.Create_RenderNode(gridSceneNode); node.Scale = Float3(HEX_SIZE, 1, HEX_SIZE); @@ -131,6 +131,10 @@ class MicrobeEditor{ LOG_INFO("Starting microbe editor with: " + editedMicrobe.length() + " organelles in the microbe"); + // Update GUI buttons now that we have correct organelles + updateGuiButtonStatus(checkIsNucleusPresent()); + //force an auto-evo step + cast(GetThriveGame().getCellStage().GetScriptSystem("SpeciesSystem")).doAutoEvoStep(); // Reset to cytoplasm if nothing is selected if(activeActionName == ""){ LOG_INFO("Selecting cytoplasm"); @@ -741,7 +745,8 @@ class MicrobeEditor{ PlacedOrganelle@ organelle = cast(organelleHere); if(organelleHere !is null){ - if(!(organelleHere.organelle.name == "nucleus")) { + // DOnt allow deletion of nucleus or the last organelle + if(!(organelleHere.organelle.name == "nucleus") && getMicrobeSize() > 1) { EditorAction@ action = EditorAction(ORGANELLE_REMOVE_COST, // redo We need data about the organelle we removed, and the location so we can "redo" it function(EditorAction@ action, MicrobeEditor@ editor){ diff --git a/scripts/microbe_stage/microbe_operations.as b/scripts/microbe_stage/microbe_operations.as index 9e0fbfa6a55..24b3decdc7a 100644 --- a/scripts/microbe_stage/microbe_operations.as +++ b/scripts/microbe_stage/microbe_operations.as @@ -295,6 +295,11 @@ void respawnPlayer(CellStageWorld@ world) // Reset membrane color to fix the bug that made membranes sometimes red after you respawn. MicrobeOperations::applyMembraneColour(world, playerEntity); + //If you died before entering the editor disable that + microbeComponent.reproductionStage = 0; + hideReproductionDialog(world); + // Reset the player cell to be the same as the species template + Species::restoreOrganelleLayout(world, playerEntity, microbeComponent, playerSpecies); } // Decrease the population by 20 @@ -694,7 +699,8 @@ void playSoundWithDistance(CellStageWorld@ world, const string &in soundPath, Ob "sound source"); } } else { - LOG_ERROR("Failed to create sound player"); + //this was happening so often it caused lag + //LOG_ERROR("Failed to create sound player"); } } } @@ -927,7 +933,8 @@ ObjectID spawnBacteria(CellStageWorld@ world, Float3 pos, const string &in speci void _applyMicrobeCollisionShape(CellStageWorld@ world, Physics@ rigidBody, MicrobeComponent@ microbeComponent, PhysicsShape@ shape) { - float mass = 0.f; + // This compensates for the lack of a nucleus for the player cell at the beginning and makes eukaryotes alot heavier. + float mass = 0.7f; // Organelles for(uint i = 0; i < microbeComponent.organelles.length(); ++i){ diff --git a/scripts/microbe_stage/microbe_stage_hud.as b/scripts/microbe_stage/microbe_stage_hud.as index 5564f1c7efe..20a429504e7 100644 --- a/scripts/microbe_stage/microbe_stage_hud.as +++ b/scripts/microbe_stage/microbe_stage_hud.as @@ -74,6 +74,10 @@ class MicrobeStageHudSystem : ScriptSystem{ this.hydrogenSulfideVolume = SimulationParameters::compoundRegistry().getTypeData( this.hydrogenSulfideId).volume; + this.ironId = SimulationParameters::compoundRegistry().getTypeId("iron"); + this.ironVolume = SimulationParameters::compoundRegistry().getTypeData( + this.ironId).volume; + } void handleAmbientSound() @@ -160,6 +164,9 @@ class MicrobeStageHudSystem : ScriptSystem{ const auto oxytoxyAmount = bag.getCompoundAmount(oxytoxyId); const auto maxOxytoxy = microbeComponent.capacity; + const auto ironAmount = bag.getCompoundAmount(ironId); + const auto maxIron = microbeComponent.capacity; + // Write data vars.AddValue(ScriptSafeVariableBlock("compoundPhosphate", phosphateAmount)); vars.AddValue(ScriptSafeVariableBlock("PhosphateMax", maxPhosphate)); @@ -178,6 +185,9 @@ class MicrobeStageHudSystem : ScriptSystem{ vars.AddValue(ScriptSafeVariableBlock("compoundOxytoxy", oxytoxyAmount)); vars.AddValue(ScriptSafeVariableBlock("OxytoxyMax", maxOxytoxy)); + + vars.AddValue(ScriptSafeVariableBlock("compoundIron", ironAmount)); + vars.AddValue(ScriptSafeVariableBlock("IronMax", maxIron)); } // Fire it off so that the GUI scripts will get it and update the GUI state @@ -276,6 +286,14 @@ class MicrobeStageHudSystem : ScriptSystem{ } } + void hideReproductionDialog(){ + reproductionDialogOpened = false; + GenericEvent@ event = GenericEvent("PlayerDiedBeforeEnter"); + GetEngine().GetEventHandler().CallEvent(event); + } + + + void suicideButtonClicked(){ // getComponent("gui_sounds", this.gameState, SoundSourceComponent).playSound("button-hover-click"); if(boolean2 == false){ @@ -398,6 +416,9 @@ class MicrobeStageHudSystem : ScriptSystem{ CompoundId oxytoxyId; float oxytoxyVolume; + CompoundId ironId; + float ironVolume; + } @@ -796,6 +817,11 @@ void showReproductionDialog(GameWorld@ world){ showReproductionDialog(); } +void hideReproductionDialog(GameWorld@ world){ + cast(world.GetScriptSystem("MicrobeStageHudSystem")). + hideReproductionDialog(); +} + void showMessage(const string &in msg){ LOG_INFO(msg + " (note, in-game messages currently disabled)"); //auto messagePanel = Engine.currentGameState().rootGUIWindow().getChild("MessagePanel") diff --git a/scripts/microbe_stage/organelle_components/movement_organelle.as b/scripts/microbe_stage/organelle_components/movement_organelle.as index 1066481b112..162d7df41ae 100644 --- a/scripts/microbe_stage/organelle_components/movement_organelle.as +++ b/scripts/microbe_stage/organelle_components/movement_organelle.as @@ -7,11 +7,11 @@ // See organelle_component.as for more information about the // organelle component methods and the arguments they receive. -// Calculate the momentum of the movement organelle based on angle towards nucleus +// Calculate the momentum of the movement organelle based on angle towards middle of cell Float3 calculateForce(int q, int r, float momentum){ Float3 organelle = Hex::axialToCartesian(q, r); - Float3 nucleus = Hex::axialToCartesian(0, 0); - auto delta = nucleus - organelle; + Float3 middle = Hex::axialToCartesian(0, 0); + auto delta = middle - organelle; return delta.Normalize() * momentum; } @@ -42,9 +42,9 @@ class MovementOrganelle : OrganelleComponent{ this.force = calculateForce(q, r, this.force.X); this.organellePos = Hex::axialToCartesian(q, r); - Float3 nucleus = Hex::axialToCartesian(0, 0); + Float3 middle = Hex::axialToCartesian(0, 0); - auto delta = nucleus - organellePos; + auto delta = middle - organellePos; float angle = atan2(-delta.Z, delta.X); if(angle < 0){ @@ -176,9 +176,9 @@ class MovementOrganelle : OrganelleComponent{ // This is because GetExternalOrganelle only works after the membrane has initialized, // which happens on the next tick // This doesnt work properly - Float3 nucleus = Hex::axialToCartesian(0, 0); - auto delta = nucleus - organellePos; - const Float3 exit = nucleus - delta; + Float3 middle = Hex::axialToCartesian(0, 0); + auto delta = middle - organellePos; + const Float3 exit = middle - delta; auto membraneComponent = organelle.world.GetComponent_MembraneComponent(microbeEntity); auto membraneCoords = membraneComponent.GetExternalOrganelle(exit.X, exit.Z);; float angle = atan2(-delta.Z, delta.X); diff --git a/scripts/microbe_stage/organelle_table.as b/scripts/microbe_stage/organelle_table.as index 0b8da35cccb..f1a2b0bbc2e 100644 --- a/scripts/microbe_stage/organelle_table.as +++ b/scripts/microbe_stage/organelle_table.as @@ -677,6 +677,31 @@ void setupOrganelles(){ _addOrganelleToTable(Organelle(nitrogenFixationProtein)); + // Rusticyanin + auto rusticyanin = OrganelleParameters("rusticyanin"); + + rusticyanin.mass = 0.1; + rusticyanin.gene = "f"; + rusticyanin.mesh = "chemoproteins.mesh"; + rusticyanin.chanceToCreate = 0.5f; + rusticyanin.prokaryoteChance = 1; + rusticyanin.mpCost = 45; + rusticyanin.initialComposition = { + {"phosphates", 1}, + {"ammonia", 1} + }; + rusticyanin.components = { + processorOrganelleFactory(1.0f), + storageOrganelleFactory(5.0f) + }; + rusticyanin.processes = { + TweakedProcess("iron_chemolithoautotrophy", 1) + }; + rusticyanin.hexes = { + Int2(0, 0), + }; + + _addOrganelleToTable(Organelle(rusticyanin)); // ------------------------------------ // // Setup the organelle letters setupOrganelleLetters(); diff --git a/scripts/microbe_stage/setup.as b/scripts/microbe_stage/setup.as index 58470154b68..4993e4b910a 100644 --- a/scripts/microbe_stage/setup.as +++ b/scripts/microbe_stage/setup.as @@ -269,7 +269,10 @@ void onReturnFromEditor(CellStageWorld@ world) } +// // TODO: also put these physics callback somewhere more sensible (maybe physics_callbacks.as?) +// + void cellHitFloatingOrganelle(GameWorld@ world, ObjectID firstEntity, ObjectID secondEntity) { // Determine which is the organelle @@ -294,6 +297,25 @@ void cellHitFloatingOrganelle(GameWorld@ world, ObjectID firstEntity, ObjectID s world.QueueDestroyEntity(floatingEntity); } + +void cellHitIron(GameWorld@ world, ObjectID firstEntity, ObjectID secondEntity) +{ + // Determine which is the iron + CellStageWorld@ asCellWorld = cast(world); + + auto model = asCellWorld.GetComponent_Model(firstEntity); + auto floatingEntity = firstEntity; + auto cellEntity = secondEntity; + + // Cell doesn't have a model + if(model is null){ + + @model = asCellWorld.GetComponent_Model(secondEntity); + floatingEntity = secondEntity; + cellEntity = firstEntity; + } +} + // Cell Hit Oxytoxy // We can make this generic using the dictionary in agents.as // eventually, but for now all we have is oxytoxy @@ -592,36 +614,72 @@ ObjectID createToxin(CellStageWorld@ world, Float3 pos) return toxinEntity; } -ObjectID createChloroplast(CellStageWorld@ world, Float3 pos) +ObjectID createIron(CellStageWorld@ world, Float3 pos) { - // Chloroplasts - ObjectID chloroplastEntity = world.CreateEntity(); + // Iron + ObjectID ironEntity = world.CreateEntity(); - auto position = world.Create_Position(chloroplastEntity, pos, + auto position = world.Create_Position(ironEntity, pos, Ogre::Quaternion(Ogre::Degree(GetEngine().GetRandom().GetNumber(0, 360)), Ogre::Vector3(0,1,1))); - auto renderNode = world.Create_RenderNode(chloroplastEntity); + auto renderNode = world.Create_RenderNode(ironEntity); renderNode.Scale = Float3(1, 1, 1); renderNode.Marked = true; renderNode.Node.setOrientation(Ogre::Quaternion( Ogre::Degree(GetEngine().GetRandom().GetNumber(0, 360)), Ogre::Vector3(0,1,1))); renderNode.Node.setPosition(pos); - auto model = world.Create_Model(chloroplastEntity, renderNode.Node, "chloroplast.mesh"); + string mesh=""; + int ironSize = 1; + // 5 is the default + float ironAmount = 3.0f; + double ironBagAmount= IRON_PER_SMALL_CHUNK; + // There are four kinds + switch (GetEngine().GetRandom().GetNumber(0, 4)) + { + case 0: + mesh="iron_01.mesh"; + break; + case 1: + mesh="iron_02.mesh"; + break; + case 2: + mesh="iron_03.mesh"; + break; + case 3: + mesh="iron_04.mesh"; + break; + case 4: + mesh="iron_05.mesh"; + ironSize=10; + ironAmount=10.0f; + ironBagAmount=IRON_PER_BIG_CHUNK; + break; + } + + + auto venter = world.Create_CompoundVenterComponent(ironEntity); + // So that larger iron chunks give out more compounds + venter.setVentAmount(ironAmount); + auto bag = world.Create_CompoundBagComponent(ironEntity); + + bag.setCompound(SimulationParameters::compoundRegistry().getTypeId("iron"),ironBagAmount); + auto model = world.Create_Model(ironEntity, renderNode.Node, mesh); // Need to set the tint model.GraphicalObject.setCustomParameter(1, Ogre::Vector4(1, 1, 1, 1)); - auto rigidBody = world.Create_Physics(chloroplastEntity, position); + auto rigidBody = world.Create_Physics(ironEntity, position); auto body = rigidBody.CreatePhysicsBody(world.GetPhysicalWorld(), - world.GetPhysicalWorld().CreateSphere(1), 1, - world.GetPhysicalMaterial("floatingOrganelle")); + world.GetPhysicalWorld().CreateSphere(ironSize),100, + //iron + world.GetPhysicalMaterial("iron")); body.ConstraintMovementAxises(); rigidBody.JumpTo(position); - return chloroplastEntity; + return ironEntity; } // TODO: the player species handling would be more logically placed if @@ -663,13 +721,13 @@ void setupFloatingOrganelles(CellStageWorld@ world){ LOG_INFO("setting up free floating organelles"); SpawnSystem@ spawnSystem = world.GetSpawnSystem(); - //spawn toxin and chloroplasts - const auto chloroId = spawnSystem.addSpawnType( - @createChloroplast, DEFAULT_SPAWN_DENSITY, - MICROBE_SPAWN_RADIUS); - - //toxins + // toxins const auto toxinId = spawnSystem.addSpawnType( @createToxin, DEFAULT_SPAWN_DENSITY, MICROBE_SPAWN_RADIUS); + + // iron + const auto ironId = spawnSystem.addSpawnType( + @createIron, DEFAULT_SPAWN_DENSITY, + MICROBE_SPAWN_RADIUS); } diff --git a/scripts/microbe_stage/species_system.as b/scripts/microbe_stage/species_system.as index d4c336cc09b..7ad22f3f436 100644 --- a/scripts/microbe_stage/species_system.as +++ b/scripts/microbe_stage/species_system.as @@ -89,34 +89,40 @@ string mutateWord(string name){ switch (GetEngine().GetRandom().GetNumber(0,5)) { case 0: + //VC newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable= ""+vowels.substr(newVowel, 1)+consonants.substr(newConsonant, 1); newName.insert(index, newSyllable); break; case 1: + //CV newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable = ""+consonants.substr(newConsonant, 1)+vowels.substr(newVowel, 1); newName.insert(index, newSyllable); break; case 2: + //CC newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable= ""+original+consonants.substr(newConsonant, 1); newName.insert(index, newSyllable); break; case 3: + //CC newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable = ""+consonants.substr(newConsonant, 1)+original; newName.insert(index, newSyllable); break; case 4: + //CCV newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable = original+consonants.substr(newConsonant, 1)+vowels.substr(newVowel, 1); newName.insert(index, newSyllable); break; case 5: + //VCC newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); newSyllable = vowels.substr(newVowel, 1)+consonants.substr(newConsonant, 1)+original; @@ -124,14 +130,17 @@ string mutateWord(string name){ break; } } + // If is vowel else { if(GetEngine().GetRandom().GetNumber(0,20) <= 10){ + //CVV int newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); int newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); string newSyllable = ""+consonants.substr(newConsonant, 1)+vowels.substr(newVowel, 1)+original; newName.insert(index, newSyllable); } else { + //VVC int newConsonant = GetEngine().GetRandom().GetNumber(0,consonants.length()-1); int newVowel = GetEngine().GetRandom().GetNumber(0,vowels.length()-1); string newSyllable = ""+original+vowels.substr(newVowel, 1)+consonants.substr(newConsonant, 1); @@ -247,6 +256,7 @@ string generateNameSection() return newName; } +// For normal microbes const dictionary DEFAULT_INITIAL_COMPOUNDS = { {"atp", InitialCompound(30,300)}, @@ -254,7 +264,20 @@ const dictionary DEFAULT_INITIAL_COMPOUNDS = {"ammonia", InitialCompound(30,100)}, {"phosphates", InitialCompound(0)}, {"hydrogensulfide", InitialCompound(0)}, - {"oxytoxy", InitialCompound(0)} + {"oxytoxy", InitialCompound(0)}, + {"iron", InitialCompound(0)} + }; + +// For iron phillic microbes +const dictionary DEFAULT_INITIAL_COMPOUNDS_IRON = + { + {"atp", InitialCompound(30,300)}, + {"glucose", InitialCompound(10,30)}, + {"ammonia", InitialCompound(30,100)}, + {"phosphates", InitialCompound(0)}, + {"hydrogensulfide", InitialCompound(0)}, + {"oxytoxy", InitialCompound(0)}, + {"iron", InitialCompound(30,300)} }; string randomSpeciesName() @@ -477,10 +500,19 @@ class Species{ @forWorld = world; auto organelles = positionOrganelles(stringCode); - + // If you have iron (f is the symbol for rusticyanin) + if (stringCode.findFirst('f') >= 0) + { + templateEntity = Species::createSpecies(forWorld, this.name, this.genus, this.epithet, + organelles, this.colour, this.isBacteria, this.speciesMembraneType, + DEFAULT_INITIAL_COMPOUNDS_IRON, this.aggression, this.fear, this.activity, this.focus); + } + else { templateEntity = Species::createSpecies(forWorld, this.name, this.genus, this.epithet, organelles, this.colour, this.isBacteria, this.speciesMembraneType, DEFAULT_INITIAL_COMPOUNDS, this.aggression, this.fear, this.activity, this.focus); + } + } // Delete a species @@ -703,7 +735,7 @@ class Species{ // respiratory Proteins, or Oxy Toxy Producing Proteins, also pure cytoplasm // aswell for variety. //TODO when chemosynthesis is added add a protein for that aswell - switch(GetEngine().GetRandom().GetNumber(1,7)) + switch(GetEngine().GetRandom().GetNumber(1,8)) { case 1: stringCode = getOrganelleDefinition("protoplasm").gene; @@ -723,6 +755,10 @@ class Species{ case 6: stringCode = getOrganelleDefinition("nitrogenFixationProteins").gene; break; + case 7: + stringCode = getOrganelleDefinition("rusticyanin").gene; + stringCode += getOrganelleDefinition("protoplasm").gene; + break; default: stringCode = getOrganelleDefinition("protoplasm").gene; break; @@ -939,6 +975,7 @@ class SpeciesSystem : ScriptSystem{ resetAutoEvo(); } + void Run() { //LOG_INFO("autoevo running"); @@ -946,155 +983,146 @@ class SpeciesSystem : ScriptSystem{ timeSinceLastCycle++; while(this.timeSinceLastCycle > SPECIES_SIM_INTERVAL){ - LOG_INFO("Processing Auto-evo Step"); - this.timeSinceLastCycle -= SPECIES_SIM_INTERVAL; - bool ranEventThisStep=false; - - // Every 8 steps or so do a cambrian explosion style - // Event, this should increase variablility significantly - if(GetEngine().GetRandom().GetNumber(0, 200) <= 25){ - LOG_INFO("Cambrian Explosion"); - ranEventThisStep = true; - // TODO: add a notification for when this happens - doCambrianExplosion(); - } - - // Various mass extinction events - // Only run one "big event" per turn - if(species.length() > MAX_SPECIES+MAX_BACTERIA && !ranEventThisStep){ - - LOG_INFO("Mass extinction time"); - // F to pay respects: TODO: add a notification for when this happens - ranEventThisStep = true; - doMassExtinction(); - } - - // Add some variability, this is a less deterministic mass - // Extinction eg, a meteor, etc. - if(GetEngine().GetRandom().GetNumber(0, 1000) == 1 && !ranEventThisStep){ - LOG_INFO("Black swan event"); - ranEventThisStep = true; - // F to pay respects: TODO: add a notification for when this happens - doMassExtinction(); - } + doAutoEvoStep(); + } + } - // Super extinction event - if(GetEngine().GetRandom().GetNumber(0, 1000) == 1 && !ranEventThisStep){ - LOG_INFO("Great Dying"); - ranEventThisStep = true; - // Do mass extinction code then devastate all species, - //this should extinct quite a few of the ones that - //arent doing well. *Shudders* - doMassExtinction(); - doDevestation(); - } + void doAutoEvoStep(){ + LOG_INFO("Processing Auto-evo Step"); + this.timeSinceLastCycle -= SPECIES_SIM_INTERVAL; + bool ranEventThisStep=false; + // Every 8 steps or so do a cambrian explosion style + // Event, this should increase variablility significantly + if(GetEngine().GetRandom().GetNumber(0, 200) <= 25){ + LOG_INFO("Cambrian Explosion"); + ranEventThisStep = true; + // TODO: add a notification for when this happens + doCambrianExplosion(); + } - auto numberOfSpecies = species.length(); - for(uint i = 0; i < numberOfSpecies; i++){ - // Traversing the population backwards to avoid - // "chopping down the branch i'm sitting in" - auto index = numberOfSpecies - 1 - i; - auto currentSpecies = species[index]; - currentSpecies.updatePopulation(); - auto population = currentSpecies.population; - LOG_INFO(currentSpecies.name + " " + currentSpecies.population); + // Various mass extinction events + // Only run one "big event" per turn + if(species.length() > MAX_SPECIES+MAX_BACTERIA && !ranEventThisStep){ + LOG_INFO("Mass extinction time"); + // F to pay respects: TODO: add a notification for when this happens + ranEventThisStep = true; + doMassExtinction(); + } - bool ranSpeciesEvent = false; + // Add some variability, this is a less deterministic mass + // Extinction eg, a meteor, etc. + if(GetEngine().GetRandom().GetNumber(0, 1000) == 1 && !ranEventThisStep){ + LOG_INFO("Black swan event"); + ranEventThisStep = true; + // F to pay respects: TODO: add a notification for when this happens + doMassExtinction(); + } + // Super extinction event + if(GetEngine().GetRandom().GetNumber(0, 1000) == 1 && !ranEventThisStep){ + LOG_INFO("Great Dying"); + ranEventThisStep = true; + // Do mass extinction code then devastate all species, + //this should extinct quite a few of the ones that + //arent doing well. *Shudders* + doMassExtinction(); + doDevestation(); + } - // This is also just to shake things up occassionally - // Cambrian Explosion - if ( currentSpecies.population > 0 && - GetEngine().GetRandom().GetNumber(0, 10) <= 2) - { - // P to pat back: TODO: add a notification for when this happens - LOG_INFO(currentSpecies.name + " is diversifying!"); - currentSpecies.boom(); - LOG_INFO(currentSpecies.name+" population is now "+ - currentSpecies.population); - ranSpeciesEvent=true; - } + auto numberOfSpecies = species.length(); + for(uint i = 0; i < numberOfSpecies; i++){ + // Traversing the population backwards to avoid + // "chopping down the branch i'm sitting in" + auto index = numberOfSpecies - 1 - i; + auto currentSpecies = species[index]; + currentSpecies.updatePopulation(); + auto population = currentSpecies.population; + LOG_INFO(currentSpecies.name + " " + currentSpecies.population); + bool ranSpeciesEvent = false; + // This is also just to shake things up occassionally + // Cambrian Explosion + if ( currentSpecies.population > 0 && + GetEngine().GetRandom().GetNumber(0, 10) <= 2) + { + // P to pat back: TODO: add a notification for when this happens + LOG_INFO(currentSpecies.name + " is diversifying!"); + currentSpecies.boom(); + LOG_INFO(currentSpecies.name+" population is now "+ + currentSpecies.population); + ranSpeciesEvent=true; + } // This is just to shake things up occassionally - if ( currentSpecies.population > 0 && + if ( currentSpecies.population > 0 && GetEngine().GetRandom().GetNumber(0, 10) <= 2 && !ranSpeciesEvent) - { + { // F to pay respects: TODO: add a notification for when this happens LOG_INFO(currentSpecies.name + " has been devestated by disease."); currentSpecies.devestate(); LOG_INFO(currentSpecies.name+" population is now "+ currentSpecies.population); ranSpeciesEvent=true; - } + } // 50% chance of splitting off two species instead of one - if(GetEngine().GetRandom().GetNumber(0, 10) <= 5 && ranSpeciesEvent == false && - currentSpecies.population >= MAX_POP_SIZE){ - - // To prevent ridiculous population numbers - currentSpecies.population=MAX_POP_SIZE; + if(GetEngine().GetRandom().GetNumber(0, 10) <= 5 && ranSpeciesEvent == false && + currentSpecies.population >= MAX_POP_SIZE){ - auto oldPop = currentSpecies.population; - auto newSpecies = Species(currentSpecies, world, - currentSpecies.isBacteria); + // To prevent ridiculous population numbers + currentSpecies.population=MAX_POP_SIZE; + auto oldPop = currentSpecies.population; + auto newSpecies = Species(currentSpecies, world, + currentSpecies.isBacteria); - if (newSpecies.isBacteria) - { - currentBacteriaAmount+=1; + if (newSpecies.isBacteria){ + currentBacteriaAmount+=1; } - else - { - currentEukaryoteAmount+=1; + else{ + currentEukaryoteAmount+=1; } - ranSpeciesEvent=true; - species.insertLast(newSpecies); - LOG_INFO("Species " + currentSpecies.name + - " split off several species, the first is:" + - newSpecies.name); - // Reset pop so we can split a second time - currentSpecies.population = oldPop; - } - - // Reproduction and mutation - if(currentSpecies.population >= MAX_POP_SIZE){ - - // To prevent ridiculous population numbers - currentSpecies.population=MAX_POP_SIZE; + ranSpeciesEvent=true; + species.insertLast(newSpecies); + LOG_INFO("Species " + currentSpecies.name + + " split off several species, the first is:" + + newSpecies.name); + // Reset pop so we can split a second time + currentSpecies.population = oldPop; + } - auto newSpecies = Species(currentSpecies, world, - currentSpecies.isBacteria); + // Reproduction and mutation + if(currentSpecies.population >= MAX_POP_SIZE){ + // To prevent ridiculous population numbers + currentSpecies.population=MAX_POP_SIZE; - if (newSpecies.isBacteria) - { - currentBacteriaAmount+=1; - } - else - { - currentEukaryoteAmount+=1; - } - species.insertLast(newSpecies); - LOG_INFO("Species " + currentSpecies.name + " split off a child species:" + - newSpecies.name); + auto newSpecies = Species(currentSpecies, world, + currentSpecies.isBacteria); + if (newSpecies.isBacteria){ + currentBacteriaAmount+=1; + } + else{ + currentEukaryoteAmount+=1; } + species.insertLast(newSpecies); + LOG_INFO("Species " + currentSpecies.name + " split off a child species:" + + newSpecies.name); + } - // Extinction, this is not an event since things with - // low population need to be killed off. - if(currentSpecies.population <= MIN_POP_SIZE){ - LOG_INFO("Species " + currentSpecies.name + " went extinct"); - currentSpecies.extinguish(); - species.removeAt(index); - // Tweak numbers here - if (currentSpecies.isBacteria) - { - currentBacteriaAmount-=1; - } - else - { - currentEukaryoteAmount-=1; - } + // Extinction, this is not an event since things with + // low population need to be killed off. + if(currentSpecies.population <= MIN_POP_SIZE){ + LOG_INFO("Species " + currentSpecies.name + " went extinct"); + currentSpecies.extinguish(); + species.removeAt(index); + // Tweak numbers here + if (currentSpecies.isBacteria){ + currentBacteriaAmount-=1; + } + else{ + currentEukaryoteAmount-=1; } } + } // These are kind of arbitray, we should pronbabbly make it less arbitrary // New species @@ -1110,7 +1138,6 @@ class SpeciesSystem : ScriptSystem{ createBacterium(); currentBacteriaAmount++; } - } } void Clear(){} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 94a66c626f0..5318108af09 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,8 @@ set(GROUP_MICROBE_STAGE "microbe_stage/microbe_camera_system.h" "microbe_stage/process_system.cpp" "microbe_stage/process_system.h" + "microbe_stage/compound_venter_system.cpp" + "microbe_stage/compound_venter_system.h" "microbe_stage/simulation_parameters.cpp" "microbe_stage/simulation_parameters.h" "microbe_stage/spawn_system.cpp" diff --git a/src/ThriveGame.cpp b/src/ThriveGame.cpp index 140dd11f36f..5edfd828ff9 100644 --- a/src/ThriveGame.cpp +++ b/src/ThriveGame.cpp @@ -163,13 +163,19 @@ class ThriveGame::Implementation { ThriveGame::ThriveGame() { #ifndef MAKE_RELEASE - LOG_INFO("Enabling cheats because this is a non-release build"); + LOG_INFO("Enabling cheats because this is a development build"); m_cheatsEnabled = true; #endif // MAKE_RELEASE StaticGame = this; } +ThriveGame::ThriveGame(Leviathan::Engine* engine) : + Leviathan::ClientApplication(engine) +{ + StaticGame = this; +} + ThriveGame::~ThriveGame() { StaticGame = nullptr; @@ -180,7 +186,7 @@ std::string { return "Thrive " GAME_VERSIONS #ifndef MAKE_RELEASE - " (non-release)" + " (development)" #endif ; } @@ -246,6 +252,10 @@ void netSettings.IsAuthoritative = true; netSettings.DoInterpolation = true; + // TODO: switch to + // Leviathan::WorldNetworkSettings::GetSettingsForSinglePlayer() once we + // no longer do the interpolation once variable rate ticks are supported + LOG_INFO("ThriveGame: startNewGame: Creating new cellstage world"); m_impl->m_cellStage = std::dynamic_pointer_cast(engine->CreateWorld( @@ -304,10 +314,9 @@ void // This is needed for the compound clouds to work in general const auto compoundCount = SimulationParameters::compoundRegistry.getSize(); + LEVIATHAN_ASSERT(SimulationParameters::compoundRegistry.getSize() > 0, "compound registry is empty when creating cloud entities for them"); - std::unordered_map u = - {}; std::vector clouds; @@ -495,7 +504,8 @@ void m_impl->m_microbeEditor = std::dynamic_pointer_cast(engine->CreateWorld( window1, static_cast(THRIVE_WORLD_TYPE::MICROBE_EDITOR), - createPhysicsMaterials(), netSettings)); + createPhysicsMaterials(), + Leviathan::WorldNetworkSettings::GetSettingsForSinglePlayer())); } LEVIATHAN_ASSERT( @@ -985,17 +995,27 @@ void ThriveGame::Tick(int mspassed) {} -void - ThriveGame::CustomizeEnginePostLoad() +bool + ThriveGame::createImpl() { - Engine* engine = Engine::Get(); - try { m_impl = std::make_unique(*this); } catch(const Leviathan::InvalidArgument& e) { LOG_ERROR("ThriveGame: loading configuration data failed: "); e.PrintToLog(); + return false; + } + + return true; +} + +void + ThriveGame::CustomizeEnginePostLoad() +{ + Engine* engine = Engine::Get(); + + if(!createImpl()) { MarkAsClosing(); return; } @@ -1090,6 +1110,7 @@ void keyconfigobj->AddKeyIfMissing(guard, "MoveRight", {"D"}); keyconfigobj->AddKeyIfMissing(guard, "ReproduceCheat", {"P"}); keyconfigobj->AddKeyIfMissing(guard, "SpawnGlucoseCheat", {"O"}); + keyconfigobj->AddKeyIfMissing(guard, "SpawnPhosphateCheat", {"I"}); keyconfigobj->AddKeyIfMissing(guard, "EngulfMode", {"G"}); keyconfigobj->AddKeyIfMissing(guard, "ShootToxin", {"E"}); keyconfigobj->AddKeyIfMissing(guard, "Screenshot", {"PrintScreen"}); diff --git a/src/ThriveGame.h b/src/ThriveGame.h index 8b0c26b2b52..a2c522bdf92 100644 --- a/src/ThriveGame.h +++ b/src/ThriveGame.h @@ -13,6 +13,10 @@ namespace thrive { +namespace test { +class TestThriveGame; +} + class CellStageWorld; class ThriveNetHandler; @@ -25,9 +29,14 @@ class PlayerMicrobeControl; //! running the engine and the event loop class ThriveGame : public Leviathan::ClientApplication, public ThriveCommon { class Implementation; + friend test::TestThriveGame; public: ThriveGame(); + + //! \brief Version for tests with incomplete engine instance + ThriveGame(Leviathan::Engine* engine); + virtual ~ThriveGame(); // ------------------------------------ // @@ -150,6 +159,9 @@ class ThriveGame : public Leviathan::ClientApplication, public ThriveCommon { void _ShutdownApplicationPacketHandler() override; + bool + createImpl(); + private: std::unique_ptr m_network; diff --git a/src/engine/component_types.h b/src/engine/component_types.h index 6f5c0b949c8..5311d147e6f 100644 --- a/src/engine/component_types.h +++ b/src/engine/component_types.h @@ -21,6 +21,7 @@ enum class THRIVE_COMPONENT : uint16_t { ABSORBER, TIMED_LIFE, PROPERTIES, + COMPOUND_VENTER, // TODO: check is this needed for anything // INVALID diff --git a/src/general/json_registry.h b/src/general/json_registry.h index 877fc5b89e8..ea18d13f97f 100644 --- a/src/general/json_registry.h +++ b/src/general/json_registry.h @@ -17,6 +17,13 @@ // Base class of things to register. class RegistryType { public: + RegistryType() {} + + //! \brief Helper for derived test constructors + RegistryType(size_t id, const std::string& name) : + id(id), displayName(name), internalName(name) + {} + // Used to search by id. size_t id = std::numeric_limits::max(); // This would mean an error. diff --git a/src/microbe_stage/compound_cloud_system.cpp b/src/microbe_stage/compound_cloud_system.cpp index 5d728c3c067..8def5d704ee 100644 --- a/src/microbe_stage/compound_cloud_system.cpp +++ b/src/microbe_stage/compound_cloud_system.cpp @@ -47,33 +47,29 @@ CompoundCloudComponent::CompoundCloudComponent(CompoundCloudSystem& owner, "CompoundCloudComponent needs at least one Compound type"); // Read data - // Redundant check (see the throw above) - if(first) { - - m_compoundId1 = first->id; - m_color1 = - Ogre::Vector4(first->colour.r, first->colour.g, first->colour.b, 1); - } + m_color1 = + Ogre::Vector4(first->colour.r, first->colour.g, first->colour.b, 1.0f); + m_compoundId1 = first->id; if(second) { m_compoundId2 = second->id; m_color2 = Ogre::Vector4( - second->colour.r, second->colour.g, second->colour.b, 1); + second->colour.r, second->colour.g, second->colour.b, 1.0f); } if(third) { m_compoundId3 = third->id; - m_color3 = - Ogre::Vector4(third->colour.r, third->colour.g, third->colour.b, 1); + m_color3 = Ogre::Vector4( + third->colour.r, third->colour.g, third->colour.b, 1.0f); } if(fourth) { m_compoundId4 = fourth->id; m_color4 = Ogre::Vector4( - fourth->colour.r, fourth->colour.g, fourth->colour.b, 1); + fourth->colour.r, fourth->colour.g, fourth->colour.b, 1.0f); } } @@ -284,20 +280,39 @@ void m_position.X, CLOUD_Y_COORDINATE, m_position.Z); // Clear data. Maybe there is a faster way - for(size_t x = 0; x < m_density1.size(); ++x) { - for(size_t y = 0; y < m_density1[x].size(); ++y) { - - m_density1[x][y] = 0; - m_oldDens1[x][y] = 0; + if(m_compoundId1 != NULL_COMPOUND) { + for(size_t x = 0; x < m_density1.size(); ++x) { + for(size_t y = 0; y < m_density1[x].size(); ++y) { + m_density1[x][y] = 0; + m_oldDens1[x][y] = 0; + } + } + } - m_density2[x][y] = 0; - m_oldDens2[x][y] = 0; + if(m_compoundId2 != NULL_COMPOUND) { + for(size_t x = 0; x < m_density2.size(); ++x) { + for(size_t y = 0; y < m_density2[x].size(); ++y) { + m_density2[x][y] = 0; + m_oldDens2[x][y] = 0; + } + } + } - m_density3[x][y] = 0; - m_oldDens3[x][y] = 0; + if(m_compoundId3 != NULL_COMPOUND) { + for(size_t x = 0; x < m_density3.size(); ++x) { + for(size_t y = 0; y < m_density3[x].size(); ++y) { + m_density3[x][y] = 0; + m_oldDens3[x][y] = 0; + } + } + } - m_density4[x][y] = 0; - m_oldDens4[x][y] = 0; + if(m_compoundId4 != NULL_COMPOUND) { + for(size_t x = 0; x < m_density4.size(); ++x) { + for(size_t y = 0; y < m_density4[x].size(); ++y) { + m_density4[x][y] = 0; + m_oldDens4[x][y] = 0; + } } } } @@ -328,6 +343,11 @@ void const auto meshName = "CompoundCloudSystem_Plane_" + std::to_string(++CloudMeshNumberCounter); + // TODO: fix this in the engine to make this method simpler + // This crashes when used with RenderDoc and doesn't render anything + // m_planeMesh = Leviathan::GeometryHelpers::CreateXZPlane( + // meshName, CLOUD_WIDTH, CLOUD_HEIGHT); + // Create a background plane on which the fluid clouds will be drawn. m_planeMesh = Ogre::MeshManager::getSingleton().createManual( meshName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); @@ -408,7 +428,6 @@ void doSpawnCycle(world, Float3(0, 0, 0)); } -//! \brief Places specified amount of compound at position bool CompoundCloudSystem::addCloud(CompoundId compound, float density, @@ -445,7 +464,6 @@ bool return false; } -//! \param rate should be less than one. float CompoundCloudSystem::takeCompound(CompoundId compound, const Float3& worldPosition, @@ -480,7 +498,6 @@ float return 0; } -//! \param rate should be less than one. float CompoundCloudSystem::amountAvailable(CompoundId compound, const Float3& worldPosition, @@ -606,6 +623,17 @@ std::tuple return std::make_tuple(localX, localY); } +Float3 + CompoundCloudSystem::calculateGridCenterForPlayerPos(const Float3& pos) +{ + // The gaps between the positions is used for calculations here. Otherwise + // all clouds get moved when the player moves + return Float3( + static_cast(std::round(pos.X / CLOUD_X_EXTENT)) * CLOUD_X_EXTENT, + 0, + static_cast(std::round(pos.Z / CLOUD_Y_EXTENT)) * CLOUD_Y_EXTENT); +} + // ------------------------------------ // void CompoundCloudSystem::Run(CellStageWorld& world) @@ -663,194 +691,195 @@ void // Initial spawning if everything is empty if(m_managedClouds.empty()) { - LOG_INFO("CompoundCloudSystem doing initial spawning"); - m_cloudGridCenter = Float3(0, 0, 0); + const auto requiredCloudPositions{ + calculateGridPositions(m_cloudGridCenter)}; + for(size_t i = 0; i < m_cloudTypes.size(); i += CLOUDS_IN_ONE) { - // Center - _spawnCloud(world, m_cloudGridCenter, i); - - // Top left - _spawnCloud(world, - m_cloudGridCenter + - Float3(-CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), - i); - - // Up - _spawnCloud( - world, m_cloudGridCenter + Float3(0, 0, -CLOUD_HEIGHT * 2), i); - - // Top right - _spawnCloud(world, - m_cloudGridCenter + - Float3(CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), - i); - - // Left - _spawnCloud( - world, m_cloudGridCenter + Float3(-CLOUD_WIDTH * 2, 0, 0), i); - - // Right - _spawnCloud( - world, m_cloudGridCenter + Float3(CLOUD_WIDTH * 2, 0, 0), i); - - // Bottom left - _spawnCloud(world, - m_cloudGridCenter + - Float3(-CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), - i); - - // Down - _spawnCloud( - world, m_cloudGridCenter + Float3(0, 0, CLOUD_HEIGHT * 2), i); - - // Bottom right - _spawnCloud(world, - m_cloudGridCenter + - Float3(CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), - i); + // All positions + for(const auto& pos : requiredCloudPositions) { + _spawnCloud(world, pos, i); + } } } - - LEVIATHAN_ASSERT(m_managedClouds.size() == 9, + // This rounds up to the nearest multiple of 4, + // divides that by 4 and multiplies by 9 to get all the clouds we have + // (if we have 5 compounds that are clouds, we need 18 clouds, if 4 we need + // 9 etc) + LEVIATHAN_ASSERT(m_managedClouds.size() == + ((((m_cloudTypes.size() + 4 - 1) / 4 * 4) / 4) * 9), "A CompoundCloud entity has mysteriously been destroyed"); - const auto moved = playerPos - m_cloudGridCenter; + // Calculate what our center should be + const auto targetCenter = calculateGridCenterForPlayerPos(playerPos); // TODO: because we no longer check if the player has moved at least a bit // it is possible that this gets triggered very often if the player spins - // around a cloud edge, but hopefully there isn't a performance problem and - // that case can just be ignored. - // Z is used here because these are world coordinates - if(std::abs(moved.X) > CLOUD_WIDTH || std::abs(moved.Z) > CLOUD_HEIGHT) { - - // Calculate the new center - if(moved.X < -CLOUD_WIDTH) { - m_cloudGridCenter -= Float3(CLOUD_WIDTH * 2, 0, 0); - } else if(moved.X > CLOUD_WIDTH) { - m_cloudGridCenter += Float3(CLOUD_WIDTH * 2, 0, 0); - } + // around a cloud edge. - if(moved.Z < -CLOUD_HEIGHT) { - m_cloudGridCenter -= Float3(0, 0, CLOUD_HEIGHT * 2); - } else if(moved.Z > CLOUD_HEIGHT) { - m_cloudGridCenter += Float3(0, 0, CLOUD_HEIGHT * 2); - } + // This needs an extra variable to track how much the player has moved + // auto moved = playerPos - m_cloudGridCenter; - // Calculate the new positions - const Float3 requiredCloudPositions[] = { - // Center - m_cloudGridCenter, + if(m_cloudGridCenter != targetCenter) { - // Top left - m_cloudGridCenter + Float3(-CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), + m_cloudGridCenter = targetCenter; + applyNewCloudPositioning(); + } +} - // Up - m_cloudGridCenter + Float3(0, 0, -CLOUD_HEIGHT * 2), +void + CompoundCloudSystem::applyNewCloudPositioning() +{ + // Calculate the new positions + const auto requiredCloudPositions{ + calculateGridPositions(m_cloudGridCenter)}; - // Top right - m_cloudGridCenter + Float3(CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), + // Reposition clouds according to the origin + // The max amount of clouds is that all need to be moved + const size_t MAX_FAR_CLOUDS = m_managedClouds.size(); - // Left - m_cloudGridCenter + Float3(-CLOUD_WIDTH * 2, 0, 0), + // According to spec this check is superfluous, but it makes me feel + // better + if(m_tooFarAwayClouds.size() != MAX_FAR_CLOUDS) + m_tooFarAwayClouds.resize(MAX_FAR_CLOUDS); - // Right - m_cloudGridCenter + Float3(CLOUD_WIDTH * 2, 0, 0), + size_t farAwayIndex = 0; - // Bottom left - m_cloudGridCenter + Float3(-CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), + // All clouds that aren't at one of the requiredCloudPositions needs to + // be moved. Also only one from each cloud group needs to be at each + // position + for(auto iter = m_managedClouds.begin(); iter != m_managedClouds.end(); + ++iter) { - // Down - m_cloudGridCenter + Float3(0, 0, CLOUD_HEIGHT * 2), + const auto pos = iter->second->m_position; - // Bottom right - m_cloudGridCenter + Float3(CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), - }; + bool matched = false; + // Check if it is at any of the valid positions + for(size_t i = 0; i < std::size(requiredCloudPositions); ++i) { + const auto& requiredPos = requiredCloudPositions[i]; - // Reposition clouds according to the origin - // MAX of 9 clouds can ever be repositioned (this is only the case when - // respawning) - constexpr size_t MAX_FAR_CLOUDS = 9; - std::array tooFarAwayClouds; - size_t farAwayIndex = 0; + // An exact check might work but just to be safe slight + // inaccuracy is allowed here + if((pos - requiredPos).HAddAbs() < Leviathan::EPSILON) { - // All clouds that aren't at one of the requiredCloudPositions needs to - // be moved - for(auto iter = m_managedClouds.begin(); iter != m_managedClouds.end(); - ++iter) { + // TODO: this is probably not needed and can be removed + // // It also has to be the only cloud with the first + // // compound id that it has (this is used to filter out + // // multiple clouds of a group being at some position) + // bool duplicate = false; - const auto pos = iter->second->m_position; + // const auto toCheckID = iter->second->getCompoundId1(); - bool matched = false; + // for(auto iter2 = m_managedClouds.begin(); + // iter2 != m_managedClouds.end(); ++iter2) { - // Check if it is at any of the valid positions - for(size_t i = 0; i < std::size(requiredCloudPositions); ++i) { + // if(iter == iter2) + // continue; - const auto& requiredPos = requiredCloudPositions[i]; + // if(toCheckID == iter2->second->getCompoundId1()) { + // duplicate = true; + // break; + // } + // } - // An exact check might work but just to be safe slight - // inaccuracy is allowed here - if((pos - requiredPos).HAddAbs() < Leviathan::EPSILON) { - matched = true; - break; - } + // if(!duplicate) { + matched = true; + break; + //} } + } - if(!matched) { - - if(farAwayIndex >= MAX_FAR_CLOUDS) { + if(!matched) { - LOG_FATAL("CompoundCloudSystem: Logic error in calculating " - "far away clouds that need to move"); - break; - } + if(farAwayIndex >= MAX_FAR_CLOUDS) { - tooFarAwayClouds[farAwayIndex++] = iter->second; + LOG_FATAL("CompoundCloudSystem: Logic error in calculating " + "far away clouds that need to move"); + break; } + + m_tooFarAwayClouds[farAwayIndex++] = iter->second; } + } + + // Move clouds that are too far away + // We check through each position that should have a cloud and move one + // where there isn't one. This also needs to take into account the cloud + // groups - // Move clouds that are too far away - // We check through each position that should have a cloud and move one - // where there isn't one - size_t farAwayRepositionedIndex = 0; + // Loop through the cloud groups + for(size_t c = 0; c < m_cloudTypes.size(); c += CLOUDS_IN_ONE) { + const CompoundId groupType = m_cloudTypes[c].id; + + // Loop for moving clouds to all needed positions for each group for(size_t i = 0; i < std::size(requiredCloudPositions); ++i) { bool hasCloud = false; + const auto& requiredPos = requiredCloudPositions[i]; for(auto iter = m_managedClouds.begin(); iter != m_managedClouds.end(); ++iter) { const auto pos = iter->second->m_position; - // An exact check might work but just to be safe slight // inaccuracy is allowed here - if((pos - requiredPos).HAddAbs() < Leviathan::EPSILON) { - hasCloud = true; - break; + if(((pos - requiredPos).HAddAbs() < Leviathan::EPSILON)) { + + // Check that the group of the cloud is correct + if(groupType == iter->second->getCompoundId1()) { + hasCloud = true; + break; + } } } if(hasCloud) continue; - if(farAwayRepositionedIndex >= farAwayIndex) { + bool filled = false; + + // We need to find a cloud from the right group + for(size_t checkReposition = 0; checkReposition < farAwayIndex; + ++checkReposition) { + + if(m_tooFarAwayClouds[checkReposition] && + m_tooFarAwayClouds[checkReposition]->getCompoundId1() == + groupType) { + + // Found a candidate + m_tooFarAwayClouds[checkReposition]->recycleToPosition( + requiredPos); + + // Set to null to skip on next scan + m_tooFarAwayClouds[checkReposition] = nullptr; + + filled = true; + break; + } + } + + if(!filled) { LOG_FATAL("CompoundCloudSystem: Logic error in moving far " - "clouds (ran out), total to reposition: " + - std::to_string(farAwayIndex + 1) + - ", current index: " + - std::to_string(farAwayRepositionedIndex) + - ", position grid index: " + std::to_string(i)); + "clouds, didn't find any to use for needed pos"); break; } + } + } - tooFarAwayClouds[farAwayRepositionedIndex++]->recycleToPosition( - requiredPos); + // TODO: this can be removed once this has been fully confirmed to work fine + // Errors about clouds that should have been moved but haven't been + for(size_t checkReposition = 0; checkReposition < farAwayIndex; + ++checkReposition) { + if(m_tooFarAwayClouds[checkReposition]) { + LOG_FATAL( + "CompoundCloudSystem: Logic error in moving far " + "clouds, a cloud that should have been moved wasn't moved"); } } } @@ -861,28 +890,29 @@ void size_t startIndex) { auto entity = world.CreateEntity(); - Compound* first = startIndex < m_cloudTypes.size() ? &m_cloudTypes[startIndex] : nullptr; + Compound* second = startIndex + 1 < m_cloudTypes.size() ? &m_cloudTypes[startIndex + 1] : nullptr; + Compound* third = startIndex + 2 < m_cloudTypes.size() ? &m_cloudTypes[startIndex + 2] : nullptr; + Compound* fourth = startIndex + 3 < m_cloudTypes.size() ? &m_cloudTypes[startIndex + 3] : nullptr; - CompoundCloudComponent& cloud = world.Create_CompoundCloudComponent( entity, *this, first, second, third, fourth); + m_managedClouds[entity] = &cloud; // Set correct position // TODO: this should probably be made a constructor parameter cloud.m_position = pos; initializeCloud(cloud, world.GetScene()); - m_managedClouds[entity] = &cloud; } @@ -890,8 +920,6 @@ void CompoundCloudSystem::initializeCloud(CompoundCloudComponent& cloud, Ogre::SceneManager* scene) { - LOG_INFO("Initializing a new compound cloud entity"); - // All the densities if(cloud.m_compoundId1 != NULL_COMPOUND) { cloud.m_density1.resize(CLOUD_SIMULATION_WIDTH, @@ -966,22 +994,20 @@ void pass->setFragmentProgram("CompoundCloud_PS"); // Set colour parameter // - if(cloud.m_compoundId1 != NULL_COMPOUND) - pass->getFragmentProgramParameters()->setNamedConstant( - "cloudColour1", cloud.m_color1); - if(cloud.m_compoundId2 != NULL_COMPOUND) - pass->getFragmentProgramParameters()->setNamedConstant( - "cloudColour2", cloud.m_color2); - if(cloud.m_compoundId3 != NULL_COMPOUND) - pass->getFragmentProgramParameters()->setNamedConstant( - "cloudColour3", cloud.m_color3); - if(cloud.m_compoundId4 != NULL_COMPOUND) - pass->getFragmentProgramParameters()->setNamedConstant( - "cloudColour4", cloud.m_color4); + pass->getFragmentProgramParameters()->setNamedConstant( + "cloudColour1", cloud.m_color1); + pass->getFragmentProgramParameters()->setNamedConstant( + "cloudColour2", cloud.m_color2); + pass->getFragmentProgramParameters()->setNamedConstant( + "cloudColour3", cloud.m_color3); + pass->getFragmentProgramParameters()->setNamedConstant( + "cloudColour4", cloud.m_color4); // The perlin noise texture needs to be tileable. We can't do tricks with // the cloud's position + // Even though we ask for the RGBA format the actual order of pixels when + // locked for writing is something completely different cloud.m_texture = Ogre::TextureManager::getSingleton().createManual( cloud.m_textureName, "Generated", Ogre::TEX_TYPE_2D, CLOUD_SIMULATION_WIDTH, CLOUD_SIMULATION_HEIGHT, 0, Ogre::PF_BYTE_RGBA, @@ -1014,7 +1040,6 @@ void auto* densityState = pass->createTextureUnitState(); densityState->setTexture(cloud.m_texture); - // densityState->setTextureName("TestImageThing.png"); densityState->setSamplerblock(wrappedBlock); Ogre::TexturePtr texturePtr = @@ -1025,8 +1050,8 @@ void noiseState->setSamplerblock(wrappedBlock); - // Maybe compiling this here is the best place - cloud.m_planeMaterial->compile(); + // // Maybe compiling this here is the best place + // cloud.m_planeMaterial->compile(); // Needs to create a plane instance on which the material is used on cloud.m_compoundCloudsPlane = scene->createItem(m_planeMesh); @@ -1111,15 +1136,36 @@ void // This is probably branch predictor friendly to move each bunch of pixels // separately - // First channel - if(cloud.m_compoundId1 != NULL_COMPOUND) - fillCloudChannel(cloud.m_density1, 0, rowBytes, pDest); - // Second + // Due to Ogre making the pixelbox lock however it wants the order is + // actually: Ogre::PF_A8R8G8B8 + if(pixelBox.format != Ogre::PF_A8R8G8B8) { + LOG_INFO( + "Pixel format: " + Ogre::PixelUtil::getFormatName(pixelBox.format)); + LEVIATHAN_ASSERT(false, + "Ogre created texture write lock with unexpected pixel order"); + } + + // Even with that pixel format the actual channel indexes are: + // so PF_B8G8R8A8 for some reason + // R - 2 + // G - 1 + // B - 0 + // A - 3 + + if(cloud.m_compoundId1 == NULL_COMPOUND) + LEVIATHAN_ASSERT(false, "cloud with not even the first compound"); + + // First density. R goes to channel 2 (see above for the mapping) + fillCloudChannel(cloud.m_density1, 2, rowBytes, pDest); + + // Second. G - 1 if(cloud.m_compoundId2 != NULL_COMPOUND) fillCloudChannel(cloud.m_density2, 1, rowBytes, pDest); - // Etc. + // Etc. B - 0 if(cloud.m_compoundId3 != NULL_COMPOUND) - fillCloudChannel(cloud.m_density3, 2, rowBytes, pDest); + fillCloudChannel(cloud.m_density3, 0, rowBytes, pDest); + + // A - 3 if(cloud.m_compoundId4 != NULL_COMPOUND) fillCloudChannel(cloud.m_density4, 3, rowBytes, pDest); diff --git a/src/microbe_stage/compound_cloud_system.h b/src/microbe_stage/compound_cloud_system.h index ae94e5c681a..4aa8a187231 100644 --- a/src/microbe_stage/compound_cloud_system.h +++ b/src/microbe_stage/compound_cloud_system.h @@ -315,10 +315,10 @@ class CompoundCloudComponent : public Leviathan::Component { //! The color of the compound cloud. //! Every used channel must have alpha of 1. The others have alpha 0 so that //! they don't need to be worried about affecting the resulting colours - Ogre::Vector4 m_color1; - Ogre::Vector4 m_color2; - Ogre::Vector4 m_color3; - Ogre::Vector4 m_color4; + Ogre::Vector4 m_color1 = Ogre::Vector4(0, 0, 0, 0); + Ogre::Vector4 m_color2 = Ogre::Vector4(0, 0, 0, 0); + Ogre::Vector4 m_color3 = Ogre::Vector4(0, 0, 0, 0); + Ogre::Vector4 m_color4 = Ogre::Vector4(0, 0, 0, 0); //! \brief The compound id. //! \note NULL_COMPOUND means that this cloud doesn't have that slot filled @@ -443,6 +443,42 @@ class CompoundCloudSystem { convertWorldToCloudLocalForGrab(const Float3& cloudPosition, const Float3& worldPosition); + static inline auto + calculateGridPositions(const Float3& center) + { + return std::array{ + // Center + center, + + // Top left + center + Float3(-CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), + + // Up + center + Float3(0, 0, -CLOUD_HEIGHT * 2), + + // Top right + center + Float3(CLOUD_WIDTH * 2, 0, -CLOUD_HEIGHT * 2), + + // Left + center + Float3(-CLOUD_WIDTH * 2, 0, 0), + + // Right + center + Float3(CLOUD_WIDTH * 2, 0, 0), + + // Bottom left + center + Float3(-CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), + + // Down + center + Float3(0, 0, CLOUD_HEIGHT * 2), + + // Bottom right + center + Float3(CLOUD_WIDTH * 2, 0, CLOUD_HEIGHT * 2), + }; + } + + static Float3 + calculateGridCenterForPlayerPos(const Float3& pos); + protected: //! \brief Removes deleted clouds from m_managedClouds void @@ -450,9 +486,18 @@ class CompoundCloudSystem { private: //! \brief Spawns and despawns the cloud entities around the player + //! \todo This should check if the player has moved at least 10 units to + //! avoid excessive recalculations if the player goes in a circle around a + //! cloud boundary void doSpawnCycle(CellStageWorld& world, const Float3& playerPos); + //! \brief This method handles moving the clouds + //! + //! This has been separated from doSpawnCycle to be more manageable + void + applyNewCloudPositioning(); + void _spawnCloud(CellStageWorld& world, const Float3& pos, @@ -514,6 +559,9 @@ class CompoundCloudSystem { //! the best way to simulate fluid velocity std::vector> m_xVelocity; std::vector> m_yVelocity; + + //! This is here to not have to allocate memory every tick + std::vector m_tooFarAwayClouds; }; } // namespace thrive diff --git a/src/microbe_stage/compound_venter_system.cpp b/src/microbe_stage/compound_venter_system.cpp new file mode 100644 index 00000000000..77bd1e9207a --- /dev/null +++ b/src/microbe_stage/compound_venter_system.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +#include "engine/serialization.h" + +#include "general/thrive_math.h" +#include "simulation_parameters.h" + +#include "microbe_stage/compound_venter_system.h" +#include "microbe_stage/process_system.h" + +#include "generated/cell_stage_world.h" +#include + +using namespace thrive; + + +// ------------------------------------ // +// CompoundVenterComponent +CompoundVenterComponent::CompoundVenterComponent() : Leviathan::Component(TYPE) +{} + +void + CompoundVenterSystem::Run(CellStageWorld& world) +{ + if(!world.GetNetworkSettings().IsAuthoritative) + return; + + const auto logicTime = Leviathan::TICKSPEED; + + timeSinceLastCycle++; + while(timeSinceLastCycle > TIME_SCALING_FACTOR) { + timeSinceLastCycle -= TIME_SCALING_FACTOR; + for(auto& value : CachedComponents.GetIndex()) { + CompoundBagComponent& bag = std::get<0>(*value.second); + CompoundVenterComponent& venter = std::get<1>(*value.second); + // Loop through all the compounds in the storage bag and eject them + for(const auto& compound : bag.compounds) { + double compoundAmount = compound.second.amount; + CompoundId compoundId = compound.first; + if(venter.ventAmount <= compoundAmount) { + Leviathan::Position& position = std::get<2>(*value.second); + venter.ventCompound( + position, compoundId, venter.ventAmount, world); + bag.takeCompound(compoundId, venter.ventAmount); + } + } + } + } +} + +void + CompoundVenterComponent::ventCompound(Leviathan::Position& pos, + CompoundId compound, + double amount, + CellStageWorld& world) +{ + world.GetCompoundCloudSystem().addCloud( + compound, amount * 1000.0f, pos.Members._Position); +} + +void + CompoundVenterComponent::setVentAmount(float amount) +{ + this->ventAmount = amount; +} + +float + CompoundVenterComponent::getVentAmount() +{ + return this->ventAmount; +} \ No newline at end of file diff --git a/src/microbe_stage/compound_venter_system.h b/src/microbe_stage/compound_venter_system.h new file mode 100644 index 00000000000..eecaa20fbea --- /dev/null +++ b/src/microbe_stage/compound_venter_system.h @@ -0,0 +1,90 @@ +#pragma once +#include "engine/component_types.h" +#include "engine/typedefs.h" + +#include +#include +//#include +#include "process_system.h" +#include +#include + +namespace Leviathan { +class GameWorld; +} + +namespace thrive { + +class CellStageWorld; +class CompoundVenterComponent : public Leviathan::Component { +public: + CompoundVenterComponent(); + + float x, y; + float ventAmount = 5.0f; + REFERENCE_HANDLE_UNCOUNTED_TYPE(CompoundVenterComponent); + + static constexpr auto TYPE = + componentTypeConvert(THRIVE_COMPONENT::COMPOUND_VENTER); + + void + ventCompound(Leviathan::Position& pos, + CompoundId ourCompound, + double amount, + CellStageWorld& world); + + void + setVentAmount(float amount); + + float + getVentAmount(); +}; + +class CompoundVenterSystem + : public Leviathan::System> { +public: + /** + * @brief Updates the system + * @todo Make it releases a specific amount of compounds each second + */ + void + Run(CellStageWorld& world); + + void + CreateNodes( + const std::vector>& + firstdata, + const std::vector>& + seconddata, + const std::vector>& + thirdData, + const ComponentHolder& firstholder, + const ComponentHolder& secondholder, + const ComponentHolder& thirdHolder) + { + TupleCachedComponentCollectionHelper(CachedComponents, firstdata, + seconddata, thirdData, firstholder, secondholder, thirdHolder); + } + + void + DestroyNodes( + const std::vector>& + firstdata, + const std::vector>& + seconddata, + const std::vector>& + thirdData) + { + CachedComponents.RemoveBasedOnKeyTupleList(firstdata); + CachedComponents.RemoveBasedOnKeyTupleList(seconddata); + CachedComponents.RemoveBasedOnKeyTupleList(thirdData); + } + +protected: +private: + static constexpr double TIME_SCALING_FACTOR = 20; + int timeSinceLastCycle = 0; +}; +} // namespace thrive \ No newline at end of file diff --git a/src/microbe_stage/compounds.cpp b/src/microbe_stage/compounds.cpp index 4a8189b0cfc..573ed878eb1 100644 --- a/src/microbe_stage/compounds.cpp +++ b/src/microbe_stage/compounds.cpp @@ -5,6 +5,17 @@ using namespace thrive; Compound::Compound() {} +Compound::Compound(size_t id, + const std::string& name, + bool isCloud, + bool isUseful, + bool isEnvironmental, + Ogre::ColourValue colour) : + RegistryType(id, name), + isCloud(isCloud), isUseful(isUseful), isEnvironmental(isEnvironmental), + colour(colour) +{} + Compound::Compound(Json::Value value) { volume = value["volume"].asDouble(); diff --git a/src/microbe_stage/compounds.h b/src/microbe_stage/compounds.h index 2e2c83eff4f..a2f92150dfc 100644 --- a/src/microbe_stage/compounds.h +++ b/src/microbe_stage/compounds.h @@ -16,6 +16,14 @@ class Compound : public RegistryType { Compound(); + //! \brief Constructor for test use + Compound(size_t id, + const std::string& name, + bool isCloud, + bool isUseful, + bool isEnvironmental, + Ogre::ColourValue colour); + Compound(Json::Value value); }; diff --git a/src/microbe_stage/generate_cell_stage_world.rb b/src/microbe_stage/generate_cell_stage_world.rb index 2c8a0558420..db1e4defb63 100644 --- a/src/microbe_stage/generate_cell_stage_world.rb +++ b/src/microbe_stage/generate_cell_stage_world.rb @@ -21,6 +21,7 @@ generator.addInclude "microbe_stage/membrane_system.h" generator.addInclude "microbe_stage/compound_cloud_system.h" generator.addInclude "microbe_stage/process_system.h" +generator.addInclude "microbe_stage/compound_venter_system.h" generator.addInclude "microbe_stage/species_component.h" generator.addInclude "microbe_stage/spawn_system.h" generator.addInclude "microbe_stage/agent_cloud_system.h" @@ -36,6 +37,7 @@ "CellStageWorld", componentTypes: [ EntityComponent.new("ProcessorComponent", [ConstructorInfo.new([])]), EntityComponent.new("CompoundBagComponent", [ConstructorInfo.new([])]), + EntityComponent.new("CompoundVenterComponent", [ConstructorInfo.new([])]), EntityComponent.new("SpeciesComponent", [ConstructorInfo.new([ Variable.new("name", "std::string", memberaccess: "name", @@ -150,6 +152,8 @@ EntitySystem.new("ProcessSystem", ["CompoundBagComponent", "ProcessorComponent"], runtick: {group: 10, parameters: []}, visibletoscripts: true), + EntitySystem.new("CompoundVenterSystem", ["CompoundBagComponent", "CompoundVenterComponent", "Position"], + runtick: {group: 11, parameters: []}), EntitySystem.new("TimedLifeSystem", [], runtick: {group: 45, parameters: [ "ComponentTimedLifeComponent.GetIndex()" diff --git a/src/microbe_stage/microbe_editor_key_handler.cpp b/src/microbe_stage/microbe_editor_key_handler.cpp index a6ccf789f50..3dd5e78f637 100644 --- a/src/microbe_stage/microbe_editor_key_handler.cpp +++ b/src/microbe_stage/microbe_editor_key_handler.cpp @@ -1,13 +1,9 @@ #include "microbe_editor_key_handler.h" -#include "generated/microbe_editor_world.h" -#include "microbe_stage/simulation_parameters.h" - #include #include #include #include -#include using namespace thrive; diff --git a/src/microbe_stage/player_hover_info.cpp b/src/microbe_stage/player_hover_info.cpp index a2c2a01ba8a..6eafa2bce17 100644 --- a/src/microbe_stage/player_hover_info.cpp +++ b/src/microbe_stage/player_hover_info.cpp @@ -85,85 +85,94 @@ void auto microbeComponents = world.GetScriptComponentHolder("MicrobeComponent"); - // The world will keep this alive (this is released immediately to reduce - // chance of leaks) - microbeComponents->Release(); + // Only run when scripts are loaded + // TODO: move this to a sub function to not have this huge indended block + // here + if(microbeComponents) { - const auto stringType = - Leviathan::AngelScriptTypeIDResolver::Get( - Leviathan::GetCurrentGlobalScriptExecutor()); + // The world will keep this alive (this is released immediately to + // reduce chance of leaks) + microbeComponents->Release(); - // This is used to skip the player - auto controlledEntity = ThriveGame::Get()->playerData().activeCreature(); + const auto stringType = + Leviathan::AngelScriptTypeIDResolver::Get( + Leviathan::GetCurrentGlobalScriptExecutor()); - const auto& allSpecies = world.GetComponentIndex_SpeciesComponent(); + // This is used to skip the player + auto controlledEntity = + ThriveGame::Get()->playerData().activeCreature(); - auto& index = CachedComponents.GetIndex(); - for(auto iter = index.begin(); iter != index.end(); ++iter) { + const auto& allSpecies = world.GetComponentIndex_SpeciesComponent(); - const float distance = - (std::get<1>(*iter->second).Members._Position - lookPoint).Length(); + auto& index = CachedComponents.GetIndex(); + for(auto iter = index.begin(); iter != index.end(); ++iter) { - // Find only cells that have the mouse position within their membrane - if(distance > - std::get<0>(*iter->second).calculateEncompassingCircleRadius()) - continue; + const float distance = + (std::get<1>(*iter->second).Members._Position - lookPoint) + .Length(); - // Skip player - if(iter->first == controlledEntity) - continue; + // Find only cells that have the mouse position within their + // membrane + if(distance > + std::get<0>(*iter->second).calculateEncompassingCircleRadius()) + continue; - // Hovered over this. Find the name of the species - auto microbeComponent = microbeComponents->Find(iter->first); + // Skip player + if(iter->first == controlledEntity) + continue; - if(!microbeComponent) - continue; + // Hovered over this. Find the name of the species + auto microbeComponent = microbeComponents->Find(iter->first); - // We don't store the reference to the object. The holder will keep - // the reference alive while we work on it - microbeComponent->Release(); + if(!microbeComponent) + continue; - if(microbeComponent->GetPropertyCount() < 1) { + // We don't store the reference to the object. The holder will keep + // the reference alive while we work on it + microbeComponent->Release(); - LOG_ERROR("PlayerHoverInfoSystem: Run: MicrobeComponent object " - "has no properties"); - continue; - } + if(microbeComponent->GetPropertyCount() < 1) { - if(microbeComponent->GetPropertyTypeId(0) != stringType || - std::strncmp(microbeComponent->GetPropertyName(0), "speciesName", - sizeof("speciesName") - 1) != 0) { + LOG_ERROR("PlayerHoverInfoSystem: Run: MicrobeComponent object " + "has no properties"); + continue; + } - LOG_ERROR( - "PlayerHoverInfoSystem: Run: MicrobeComponent object doesn't " - "have \"string speciesName\" as the first property"); - continue; - } + if(microbeComponent->GetPropertyTypeId(0) != stringType || + std::strncmp(microbeComponent->GetPropertyName(0), + "speciesName", sizeof("speciesName") - 1) != 0) { - const auto* name = static_cast( - microbeComponent->GetAddressOfProperty(0)); + LOG_ERROR("PlayerHoverInfoSystem: Run: MicrobeComponent object " + "doesn't " + "have \"string speciesName\" as the first property"); + continue; + } - bool found = false; + const auto* name = static_cast( + microbeComponent->GetAddressOfProperty(0)); - for(const auto& tuple : allSpecies) { + bool found = false; - SpeciesComponent* species = std::get<1>(tuple); + for(const auto& tuple : allSpecies) { - if(species->name == *name) { + SpeciesComponent* species = std::get<1>(tuple); - hovered->PushValue( - std::make_unique(new Leviathan::StringBlock( - species->genus + " " + species->epithet))); - found = true; - break; + if(species->name == *name) { + + hovered->PushValue(std::make_unique( + new Leviathan::StringBlock( + species->genus + " " + species->epithet))); + found = true; + break; + } } - } - if(!found) { + if(!found) { - // If we can't find the species, assume that it is extinct - hovered->PushValue(std::make_unique( - new Leviathan::StringBlock("Extinct(" + *name + ")"))); + // If we can't find the species, assume that it is extinct + hovered->PushValue(std::make_unique( + new Leviathan::StringBlock("Extinct(" + *name + ")"))); + } } } diff --git a/src/microbe_stage/player_microbe_control.cpp b/src/microbe_stage/player_microbe_control.cpp index 9db2102803f..b86b9a7af9f 100644 --- a/src/microbe_stage/player_microbe_control.cpp +++ b/src/microbe_stage/player_microbe_control.cpp @@ -24,6 +24,8 @@ PlayerMicrobeControl::PlayerMicrobeControl(KeyConfiguration& keys) : m_left(keys.ResolveControlNameToFirstKey("MoveLeft")), m_right(keys.ResolveControlNameToFirstKey("MoveRight")), m_spawnGlucoseCheat(keys.ResolveControlNameToFirstKey("SpawnGlucoseCheat")), + m_spawnPhosphateCheat( + keys.ResolveControlNameToFirstKey("SpawnPhosphateCheat")), m_zoomIn(keys.ResolveControlNameToKeyVector("ZoomIn")), m_zoomOut(keys.ResolveControlNameToKeyVector("ZoomOut")) {} @@ -43,6 +45,13 @@ bool return true; } + if(!active && cheatPhosphateCloudsDown && + m_spawnPhosphateCheat.Match(key, modifiers)) { + + cheatPhosphateCloudsDown = false; + return true; + } + if(!active) return false; @@ -77,6 +86,14 @@ bool cheatCloudsDown = true; } return true; + } else if(m_spawnPhosphateCheat.Match(key, modifiers)) { + + if(ThriveGame::get()->areCheatsEnabled()) { + + LOG_INFO("Phosphate cloud cheat pressed"); + cheatPhosphateCloudsDown = true; + } + return true; } @@ -256,8 +273,14 @@ void auto module = thrive->getMicrobeScripts(); - if(!module) - LOG_FATAL("PlayerMicrobeControlSystem: microbe scripts aren't loaded"); + if(!module) { + // Skip here to allow running better in unit tests + // This makes finding errors about this a bit more difficult but the + // game shouldn't start with invalid scripts + // LOG_FATAL("PlayerMicrobeControlSystem: microbe scripts aren't + // loaded"); + return; + } // Debug for movement keys // std::stringstream msg; @@ -308,17 +331,27 @@ void if(thrive->getPlayerInput()->getSpamClouds()) { - LOG_INFO("Spawning cheat cloud"); world.GetCompoundCloudSystem().addCloud( SimulationParameters::compoundRegistry.getTypeId("glucose"), 15000, lookPoint); } + + if(thrive->getPlayerInput()->getCheatPhosphateCloudsDown()) { + + world.GetCompoundCloudSystem().addCloud( + SimulationParameters::compoundRegistry.getTypeId("phosphates"), + 15000, lookPoint); + } } // ------------------------------------ // Float3 PlayerMicrobeControlSystem::getTargetPoint( Leviathan::GameWorld& worldWithCamera) { + // Skip when there is no window to allow running headless + if(!Engine::Get()->GetWindowEntity()) + return Float3(0, 0, 0); + float x, y; Engine::Get()->GetWindowEntity()->GetNormalizedRelativeMouse(x, y); diff --git a/src/microbe_stage/player_microbe_control.h b/src/microbe_stage/player_microbe_control.h index 0b60e553142..4da3705dea2 100644 --- a/src/microbe_stage/player_microbe_control.h +++ b/src/microbe_stage/player_microbe_control.h @@ -47,6 +47,12 @@ class PlayerMicrobeControl : public Leviathan::InputReceiver { return cheatCloudsDown; } + inline bool + getCheatPhosphateCloudsDown() const + { + return cheatPhosphateCloudsDown; + } + bool getPressedEngulf() const { @@ -88,6 +94,7 @@ class PlayerMicrobeControl : public Leviathan::InputReceiver { Leviathan::GKey m_right; Leviathan::GKey m_spawnGlucoseCheat; + Leviathan::GKey m_spawnPhosphateCheat; std::vector m_zoomIn; std::vector m_zoomOut; @@ -103,6 +110,9 @@ class PlayerMicrobeControl : public Leviathan::InputReceiver { //! True when cheat clouds should be spawned all the time bool cheatCloudsDown = false; + //! Phosphate cheat cloud + bool cheatPhosphateCloudsDown = false; + //! Set to false when not in the microbe stage (or maybe editor as //! well could use this) to not send control events bool m_enabled = false; diff --git a/src/scripting/script_initializer.cpp b/src/scripting/script_initializer.cpp index 6b816a227fb..b250a7a5197 100644 --- a/src/scripting/script_initializer.cpp +++ b/src/scripting/script_initializer.cpp @@ -431,6 +431,8 @@ bool static uint16_t ProcessorComponentTYPEProxy = static_cast(ProcessorComponent::TYPE); +static uint16_t CompoundVenterTYPEProxy = + static_cast(CompoundVenterComponent::TYPE); static uint16_t SpawnedComponentTYPEProxy = static_cast(SpawnedComponent::TYPE); static uint16_t AgentCloudComponentTYPEProxy = @@ -504,7 +506,29 @@ bool asMETHOD(ProcessorComponent, getCapacity), asCALL_THISCALL) < 0) { ANGELSCRIPT_REGISTERFAIL; } + // ------------------------------------ // + if(engine->RegisterObjectType( + "CompoundVenterComponent", 0, asOBJ_REF | asOBJ_NOCOUNT) < 0) { + ANGELSCRIPT_REGISTERFAIL; + } + + if(!bindComponentTypeId( + engine, "CompoundVenterComponent", &CompoundVenterTYPEProxy)) + return false; + if(engine->RegisterObjectMethod("CompoundVenterComponent", + "float getVentAmount()", + asMETHOD(CompoundVenterComponent, getVentAmount), + asCALL_THISCALL) < 0) { + ANGELSCRIPT_REGISTERFAIL; + } + + if(engine->RegisterObjectMethod("CompoundVenterComponent", + "void setVentAmount(float amount)", + asMETHOD(CompoundVenterComponent, setVentAmount), + asCALL_THISCALL) < 0) { + ANGELSCRIPT_REGISTERFAIL; + } // ------------------------------------ // if(engine->RegisterObjectType( "SpawnedComponent", 0, asOBJ_REF | asOBJ_NOCOUNT) < 0) { diff --git a/src/thrive_common.cpp b/src/thrive_common.cpp index 7e034f262b6..66706e4a394 100644 --- a/src/thrive_common.cpp +++ b/src/thrive_common.cpp @@ -146,6 +146,24 @@ void LOG_ERROR("Failed to run script side cellHitFloatingOrganelle"); } +void + cellHitIron(Leviathan::PhysicalWorld& physicalWorld, + Leviathan::PhysicsBody& first, + Leviathan::PhysicsBody& second) +{ + GameWorld* gameWorld = physicalWorld.GetGameWorld(); + + ScriptRunningSetup setup("cellHitIron"); + + auto result = + ThriveCommon::get()->getMicrobeScripts()->ExecuteOnModule(setup, + false, gameWorld, first.GetOwningEntity(), + second.GetOwningEntity()); + + if(result.Result != SCRIPT_RUN_RESULT::Success) + LOG_ERROR("Failed to run script side cellHitIron"); +} + //! \todo This should return false when either cell is engulfing and apply the //! damaging effect bool @@ -246,15 +264,23 @@ std::unique_ptr std::make_unique("floatingOrganelle", 2); auto agentMaterial = std::make_unique("agentCollision", 3); + auto ironMaterial = + std::make_unique("iron", 4); // Set callbacks // // Floating organelles cellMaterial->FormPairWith(*floatingOrganelleMaterial) .SetCallbacks(nullptr, cellHitFloatingOrganelle); + + // Iron + cellMaterial->FormPairWith(*ironMaterial) + .SetCallbacks(nullptr, cellHitIron); + // Agents cellMaterial->FormPairWith(*agentMaterial) .SetCallbacks(agentCallback, agentCollided); + // Engulfing cellMaterial->FormPairWith(*cellMaterial) .SetCallbacks(cellOnCellAABBHitCallback, cellOnCellActualContact); @@ -264,6 +290,7 @@ std::unique_ptr manager->LoadedMaterialAdd(std::move(cellMaterial)); manager->LoadedMaterialAdd(std::move(floatingOrganelleMaterial)); manager->LoadedMaterialAdd(std::move(agentMaterial)); + manager->LoadedMaterialAdd(std::move(ironMaterial)); return manager; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index deeaa041d46..528a7655048 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -8,6 +8,8 @@ include_directories("${LEVIATHAN_SRC}") set(CurrentProjectName ThriveTest) set(AllProjectFiles "test_main.cpp" + "test_thrive_game.h" + "test_script_compile.cpp" "test_simulation_parameters.cpp" "test_clouds.cpp" diff --git a/test/engine.cpp b/test/engine.cpp deleted file mode 100644 index b54e088f359..00000000000 --- a/test/engine.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "engine/engine.h" - -#include "engine/entity_manager.h" -#include "engine/tests/test_component.h" -#include "util/make_unique.h" - -#include -#include - -using namespace thrive; - -namespace { - -class TestEngine : public Engine { -public: - TestEngine(EntityManager& entityManager) : Engine(entityManager) {} -}; - -} // namespace diff --git a/test/entity.cpp b/test/entity.cpp deleted file mode 100644 index 1b9df70bc15..00000000000 --- a/test/entity.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "engine/entity.h" - -#include "engine/engine.h" -#include "engine/entity_manager.h" -#include "engine/game_state.h" -#include "engine/system.h" -#include "engine/tests/test_component.h" -#include "util/make_unique.h" - -#include - - -using namespace thrive; - -// TODO: this needs to be basically rewritten in Lua - -// struct EntityTest : public ::testing::Test { - -// EntityTest() -// : gameState(engine.createGameState("test", {}, -// GameState::Initializer(), "DragDropDemo")) -// { - -// } - -// Engine engine; - -// GameState* gameState = nullptr; - -// }; - -// TEST_F(EntityTest, Exists) { -// // Null Id should never exist -// Entity nullEntity(NULL_ENTITY, gameState); -// EXPECT_FALSE(nullEntity.exists()); -// // Entity without components doesn't exist either -// Entity entity(gameState); -// EXPECT_FALSE(entity.exists()); -// // Add some component, then it should exist -// entity.addComponent(make_unique>()); -// EXPECT_TRUE(entity.exists()); -// } - - -// TEST_F(EntityTest, HasComponent) { -// Entity entity(gameState); -// EXPECT_FALSE(entity.hasComponent(TestComponent<0>::TYPE_ID)); -// entity.addComponent(make_unique>()); -// EXPECT_TRUE(entity.hasComponent(TestComponent<0>::TYPE_ID)); -// } - - -// TEST_F(EntityTest, RemoveComponent) { -// Entity entity(gameState); -// EXPECT_FALSE(entity.hasComponent(TestComponent<0>::TYPE_ID)); -// entity.addComponent(make_unique>()); -// EXPECT_TRUE(entity.hasComponent(TestComponent<0>::TYPE_ID)); -// entity.removeComponent(TestComponent<0>::TYPE_ID); -// gameState->entityManager().processRemovals(); -// EXPECT_FALSE(entity.hasComponent(TestComponent<0>::TYPE_ID)); -// } - - -// TEST_F(EntityTest, NamedEntity) { -// Entity unnamed(gameState); -// Entity named("named", gameState); -// Entity namedCopy("named", gameState); -// EXPECT_FALSE(named == unnamed); -// EXPECT_TRUE(named == namedCopy); -// } diff --git a/test/entity_filter.cpp b/test/entity_filter.cpp deleted file mode 100644 index 5142fbfda56..00000000000 --- a/test/entity_filter.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "engine/entity_filter.h" - -#include "engine/entity_manager.h" -#include "engine/tests/test_component.h" -#include "util/make_unique.h" - -#include - -using namespace thrive; - -TEST(EntityFilter, Initialization) -{ - EntityManager entityManager; - // Add component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - EXPECT_TRUE(nullptr != entityManager.getComponent( - entityId, TestComponent<0>::TYPE_ID)); - // Set up filter - EntityFilter> filter; - filter.setEntityManager(&entityManager); - // Check filter - auto filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); -} - -TEST(EntityFilter, Single) -{ - EntityManager entityManager; - // Set up filter - EntityFilter> filter; - filter.setEntityManager(&entityManager); - // Add component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - // Check filter - auto filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Remove component - entityManager.removeComponent(entityId, TestComponent<0>::TYPE_ID); - entityManager.processRemovals(); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(0, filteredEntities.count(entityId)); - EXPECT_EQ(0, filteredEntities.size()); -} - - -TEST(EntityFilter, Multiple) -{ - EntityManager entityManager; - // Set up filter - EntityFilter, TestComponent<1>> filter; - filter.setEntityManager(&entityManager); - auto filteredEntities = filter.entities(); - // Add first component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - // Check filter - filteredEntities = filter.entities(); - // Add first component - EXPECT_EQ(0, filteredEntities.count(entityId)); - EXPECT_EQ(0, filteredEntities.size()); - // Add second component - entityManager.addComponent(entityId, make_unique>()); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Remove component - entityManager.removeComponent(entityId, TestComponent<1>::TYPE_ID); - entityManager.processRemovals(); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(0, filteredEntities.count(entityId)); - EXPECT_EQ(0, filteredEntities.size()); -} - - -TEST(EntityFilter, Optional) -{ - EntityManager entityManager; - using TestFilter = - EntityFilter, Optional>>; - // Set up filter - TestFilter filter; - filter.setEntityManager(&entityManager); - TestFilter::EntityMap filteredEntities = filter.entities(); - // Add first component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Check group - TestFilter::ComponentGroup group = filteredEntities[entityId]; - EXPECT_TRUE(std::get<0>(group) != nullptr); - EXPECT_TRUE(std::get<1>(group) == nullptr); - // Add second component - entityManager.addComponent(entityId, make_unique>()); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Check group - group = filteredEntities[entityId]; - EXPECT_TRUE(std::get<0>(group) != nullptr); - EXPECT_TRUE(std::get<1>(group) != nullptr); - // Remove component - entityManager.removeComponent(entityId, TestComponent<1>::TYPE_ID); - entityManager.processRemovals(); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Check group - group = filteredEntities[entityId]; - EXPECT_TRUE(std::get<0>(group) != nullptr); -} - - -TEST(EntityFilter, OptionalOnly) -{ - EntityManager entityManager; - using TestFilter = EntityFilter>>; - // Set up filter - TestFilter filter; - filter.setEntityManager(&entityManager); - TestFilter::EntityMap filteredEntities = filter.entities(); - // Add first component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(1, filteredEntities.count(entityId)); - EXPECT_EQ(1, filteredEntities.size()); - // Check group - TestFilter::ComponentGroup group = filteredEntities[entityId]; - EXPECT_TRUE(std::get<0>(group) != nullptr); - // Remove component - entityManager.removeComponent(entityId, TestComponent<0>::TYPE_ID); - entityManager.processRemovals(); - // Check filter - filteredEntities = filter.entities(); - EXPECT_EQ(0, filteredEntities.count(entityId)); - EXPECT_EQ(0, filteredEntities.size()); -} - - -TEST(EntityFilter, Record) -{ - EntityManager entityManager; - using TestFilter = EntityFilter>; - // Set up filter - TestFilter filter(true); - filter.setEntityManager(&entityManager); - TestFilter::EntityMap filteredEntities = filter.entities(); - // Add first component - EntityId entityId = entityManager.generateNewId(); - entityManager.addComponent(entityId, make_unique>()); - // Check added entities - EXPECT_EQ(1, filter.addedEntities().count(entityId)); - // Remove component - entityManager.removeComponent(entityId, TestComponent<0>::TYPE_ID); - entityManager.processRemovals(); - // Check removed entities - EXPECT_EQ(0, filter.removedEntities().count(entityId)); -} diff --git a/test/rng.cpp b/test/rng.cpp deleted file mode 100644 index bdfafb1ca4d..00000000000 --- a/test/rng.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "engine/rng.h" -#include "engine/tests/test_component.h" - -#include -#include -#include -#include - -#include - - -using namespace thrive; - - -TEST(RNG, getInt) -{ - RNG rng; - std::set rngIntValues; - // A series of random integers should not result in the same number - // repeatedly. (These test will unintentionally fail once every 1x10^200 - // times, feeling lucky?) - for(int i = 0; i < 100; ++i) - rngIntValues.insert(rng.getInt(1, 100)); - EXPECT_FALSE(rngIntValues.size() == 1); - // Random numbers produced must be in the provided range - EXPECT_TRUE((*rngIntValues.begin()) >= 1); - EXPECT_TRUE((*rngIntValues.end()) <= 100); -} - -TEST(RNG, getDouble) -{ - RNG rng; - // A series of random doubles should not result in the same number - // repeatedly. - std::set rngDoubleValues; - for(int i = 0; i < 100; ++i) - rngDoubleValues.insert(rng.getDouble(1.0, 100.0)); - EXPECT_FALSE(rngDoubleValues.size() == 1); - // Random numbers produced must be in the provided range - EXPECT_TRUE((*rngDoubleValues.begin()) >= 1.0); - EXPECT_TRUE((*rngDoubleValues.end()) <= 100.0); -} - -TEST(RNG, generateRandomSeed) -{ - RNG rng; - std::unordered_set rngSeedValues; - // A series of random seeds should not result in the same number repeatedly. - for(int i = 0; i < 100; ++i) - rngSeedValues.insert(rng.generateRandomSeed()); - EXPECT_FALSE(rngSeedValues.size() == 1); -} - -TEST(RNG, getSeed) -{ - RNG rng(1337); - EXPECT_EQ(1337, rng.getSeed()); -} - -TEST(RNG, setSeed) -{ - RNG rng(1337); - rng.setSeed(1234); - EXPECT_EQ(1234, rng.getSeed()); -} - -TEST(RNG, shuffle) -{ - RNG rng; - std::vector original{ - 5, 12, 16, 18, 19, 25, 33, 41, 53, 69, 71, 87, 90}; - std::vector shuffled(original); - rng.shuffle(shuffled.begin(), shuffled.end()); - EXPECT_TRUE(shuffled != original); -} diff --git a/test/rolling_grid.cpp b/test/rolling_grid.cpp deleted file mode 100644 index 9796f1a8ec4..00000000000 --- a/test/rolling_grid.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "engine/rolling_grid.h" -#include "gtest/gtest.h" - -using namespace thrive; - -TEST(RollingGrid, Initialization) -{ - RollingGrid grid(1920, 1080, 1); -} - -TEST(RollingGrid, Read) -{ - RollingGrid grid(1920, 1080, 1); - EXPECT_EQ(0, grid(0, 0)); // somewhere in-range - EXPECT_EQ(0, grid(15649, 986984)); // somewhere out-of-range -} - -TEST(RollingGrid, Edit) -{ - RollingGrid grid(1920, 1080, 1); - EXPECT_EQ(0, grid(20, 20)); - grid(20, 20) = 5; - // std::cout << "set (20,20) to 5" << std::endl; - EXPECT_EQ(5, grid(20, 20)); - // std::cout << "checked (20,20) == 5" << std::endl; - // make sure neighbors haven't been screwed with - for(int i = 18; i < 23; i++) { - for(int j = 18; j < 23; j++) { - if(i != 20 || j != 20) { - EXPECT_EQ(0, grid(i, j)); - } - } - } -} - -TEST(RollingGrid, SmallMove) -{ - RollingGrid grid(1920, 1080, 1); - grid(100, 100) = 1; - // std::cout << "set (100,100) to 1" << std::endl; - grid.move(1, 0); - // std::cout << "moved by (15, -12)" << std::endl; - EXPECT_EQ(1, grid(100, 100)); - // std::cout << "checked (100,100)" << std::endl; - EXPECT_EQ(0, grid(115, 88)); - EXPECT_EQ(0, grid(85, 112)); - for(int i = 0; i < 1920; i++) { - for(int j = 0; j < 1080; j++) { - int k = grid(i, j); - if(k) - std::cout << "(" << i << "," << j << ") = " << k << std::endl; - } - } -} - -TEST(RollingGrid, BigMove) -{ - RollingGrid grid(1920, 1080, 1); - grid(0, 0) = 1; - grid.move(71280, 90506); - EXPECT_EQ(0, grid(0, 0)); - grid(71281, 90507) = 1; - EXPECT_EQ(1, grid(71281, 90507)); -} - -TEST(RollingGrid, TinyGrid) -{ - RollingGrid grid(1, 2, 1); - grid(0, 0) = 1; - grid.move(0, -1); - EXPECT_EQ(1, grid(0, 0)); -} - -TEST(RollingGrid, NullMoves) -{ - RollingGrid grid(100, 200, 1); - grid(0, 0) = 1; - grid.move(0, 0); - EXPECT_EQ(1, grid(0, 0)); - - grid.move(0, -1); - grid.move(0, 1); - EXPECT_EQ(1, grid(0, 0)); -} diff --git a/test/script_bindings.cpp b/test/script_bindings.cpp deleted file mode 100644 index 800ebc7d925..00000000000 --- a/test/script_bindings.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "scripting/script_initializer.h" - -#include "scripting/luajit.h" - -#include -#include - -using namespace Ogre; -using namespace thrive; - -TEST(OgreVector3, Lua) -{ - - sol::state lua; - - initializeLua(lua); - - lua.do_string("a = Vector3(1, 2, 3)\n" - "b = Vector3(10, 20, 30)\n" - "sum = a + b\n" - "dot = a:dotProduct(b)\n"); - - Vector3 sum = lua.get("sum"); - Real dot = lua.get("dot"); - EXPECT_EQ(Vector3(11, 22, 33), sum); - EXPECT_EQ(140, dot); -} diff --git a/test/sky_system.cpp b/test/sky_system.cpp deleted file mode 100644 index 81ffee14257..00000000000 --- a/test/sky_system.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "ogre/sky_system.h" - -#include "scripting/script_initializer.h" - -#include "scripting/luajit.h" - -#include "util/make_unique.h" - -#include - -using namespace thrive; - - -TEST(SkyPlaneComponent, ScriptBindings) -{ - sol::state lua; - - initializeLua(lua); - - auto skyPlane = make_unique(); - lua["skyPlane"] = skyPlane.get(); - - ; - - // Enabled - EXPECT_TRUE(lua.do_string("skyPlane.properties.enabled = false").valid()); - - EXPECT_FALSE(skyPlane->m_properties.enabled); - // Plane.d - EXPECT_TRUE(lua.do_string("skyPlane.properties.plane.d = 42.0").valid()); - EXPECT_EQ(42.0f, skyPlane->m_properties.plane.d); -} diff --git a/test/test_clouds.cpp b/test/test_clouds.cpp index 1b1e5ea8a73..971d7921a98 100644 --- a/test/test_clouds.cpp +++ b/test/test_clouds.cpp @@ -1,9 +1,15 @@ //! Tests compound cloud operations that don't need Ogre +#include "engine/player_data.h" +#include "generated/cell_stage_world.h" #include "microbe_stage/compound_cloud_system.h" +#include "test_thrive_game.h" +#include +#include #include "catch.hpp" using namespace thrive; +using namespace thrive::test; TEST_CASE("Cloud contains check is correct", "[microbe]") { @@ -145,3 +151,475 @@ TEST_CASE("Cloud local coordinate calculation math is right", "[microbe]") Leviathan::InvalidArgument); } } + +TEST_CASE("CloudManager grid center calculation", "[microbe]") +{ + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(0, 0, 0)) == Float3(0, 0, 0)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(CLOUD_WIDTH - 1, 0, 0)) == Float3(0, 0, 0)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(CLOUD_WIDTH, 0, 0)) == Float3(CLOUD_X_EXTENT, 0, 0)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(CLOUD_WIDTH + 1, 0, 0)) == Float3(CLOUD_X_EXTENT, 0, 0)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(CLOUD_WIDTH, 0, CLOUD_HEIGHT)) == + Float3(CLOUD_X_EXTENT, 0, CLOUD_Y_EXTENT)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(CLOUD_WIDTH, 0, CLOUD_HEIGHT * 2)) == + Float3(CLOUD_X_EXTENT, 0, CLOUD_Y_EXTENT)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(-CLOUD_X_EXTENT, 0, 0)) == Float3(-CLOUD_Y_EXTENT, 0, 0)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(-CLOUD_WIDTH, 0, -CLOUD_HEIGHT)) == + Float3(-CLOUD_X_EXTENT, 0, -CLOUD_Y_EXTENT)); + + CHECK(CompoundCloudSystem::calculateGridCenterForPlayerPos(Float3( + -CLOUD_WIDTH + 1, 0, -CLOUD_HEIGHT + 1)) == Float3(0, 0, 0)); +} + +class CloudManagerTestsFixture { +public: + CloudManagerTestsFixture() + { + thrive.lightweightInit(); + + world.SetRunInBackground(true); + + // TODO: change type when the clouds are made to run again with the + // variable rate ticks + REQUIRE(world.Init( + Leviathan::WorldNetworkSettings::GetSettingsForHybrid(), nullptr)); + + // Create player pos + player = world.CreateEntity(); + + thrive.playerData().setActiveCreature(player); + + REQUIRE_NOTHROW(playerPos = &world.Create_Position(player, + Float3(0, 0, 0), Float4::IdentityQuaternion())); + } + ~CloudManagerTestsFixture() + { + world.Release(); + } + + void + setCloudsAndRunInitial(const std::vector& cloudTypes) + { + world.GetCompoundCloudSystem().registerCloudTypes(world, cloudTypes); + + // Let it run a bit + world.Tick(1); + } + + std::vector + findClouds() + { + std::vector clouds; + + for(ObjectID entity : world.GetEntities()) { + + if(entity == player) + continue; + + REQUIRE_NOTHROW(clouds.emplace_back( + &world.GetComponent_CompoundCloudComponent(entity))); + } + + return clouds; + } + + void + movePlayerXUnits(float amount) + { + // Move player + playerPos->Members._Position.X += amount; + playerPos->Marked = true; + + // And tick + world.Tick(1); + } + +protected: + Leviathan::Test::PartialEngine engine; + TestThriveGame thrive{&engine}; + Leviathan::IDFactory ids; + + CellStageWorld world{nullptr}; + + Leviathan::Position* playerPos = nullptr; + ObjectID player = NULL_OBJECT; +}; + +template +auto + simpleCloudPosCheck(const std::vector& clouds, + const PosArrayT& targetPositions) +{ + std::vector valid; + valid.resize(targetPositions.size(), false); + + for(size_t i = 0; i < targetPositions.size(); ++i) { + + const auto& pos = targetPositions[i]; + + for(CompoundCloudComponent* cloud : clouds) { + + if(cloud->getPosition() == pos) { + + CHECK(!valid[i]); + valid[i] = true; + } + } + } + + return valid; +} + +//! A pretty inefficient but simple way to calculate how many clouds are at the +//! same pos and what types +auto + calculateCloudsAtSamePos(const std::vector& clouds) +{ + std::map> counts; + + for(auto cloud : clouds) { + + std::stringstream stream; + stream << cloud->getPosition(); + + if(counts.find(stream.str()) == counts.end()) { + counts.insert(std::make_pair(stream.str(), + std::vector{cloud->getCompoundId1()})); + } else { + counts[stream.str()].push_back(cloud->getCompoundId1()); + } + } + + return counts; +} + +template +void + checkCloudsAtPos( + const std::map>& counts, + const std::array& types) +{ + CAPTURE(types); + + for(const auto& pair : counts) { + CAPTURE(pair.first); + CAPTURE(pair.second); + + CHECK(pair.second.size() == types.size()); + + std::vector foundStatuses; + foundStatuses.resize(types.size(), false); + + for(auto id : pair.second) { + for(size_t i = 0; i < types.size(); ++i) { + if(id == types[i]) { + + CHECK(!foundStatuses[i]); + foundStatuses[i] = true; + break; + } + } + } + + CAPTURE(foundStatuses); + for(bool found : foundStatuses) + CHECK(found); + } +} + +template +auto + multiOverlapCloudPosCheck( + const std::vector& clouds, + const PosArrayT& targetPositions, + const std::array& cloudFirstTypes) +{ + std::vector> valid; + + std::array falseArray; + for(bool& entry : falseArray) + entry = false; + + valid.resize(targetPositions.size(), falseArray); + + for(size_t i = 0; i < targetPositions.size(); ++i) { + + CAPTURE(i); + + const auto& pos = targetPositions[i]; + + for(CompoundCloudComponent* cloud : clouds) { + + if(cloud->getPosition() == pos) { + + CAPTURE(cloud->getCompoundId1(), pos, cloudFirstTypes); + + bool matched = false; + + // Check which group it is + for(size_t targetTypeIndex = 0; + targetTypeIndex < cloudFirstTypes.size(); + ++targetTypeIndex) { + + if(cloud->getCompoundId1() == + cloudFirstTypes[targetTypeIndex]) { + + // Position check + CHECK(!valid[i][targetTypeIndex]); + valid[i][targetTypeIndex] = true; + matched = true; + break; + } + } + + CHECK(matched); + } + } + } + + return valid; +} + +template +void + multiCloudPositionCheckHelper( + const std::vector& clouds, + const Float3& playerPos, + const std::array& cloudFirstTypes) +{ + // There needs to be 2 clouds at each position + checkCloudsAtPos(calculateCloudsAtSamePos(clouds), cloudFirstTypes); + + // And then check that update succeeded + const auto valid = multiOverlapCloudPosCheck(clouds, + CompoundCloudSystem::calculateGridPositions( + CompoundCloudSystem::calculateGridCenterForPlayerPos(playerPos)), + cloudFirstTypes); + + for(const auto& entry : valid) { + CAPTURE(entry); + for(auto subentry : entry) + CHECK(subentry); + } +} + +TEST_CASE_METHOD(CloudManagerTestsFixture, + "Cloud manager creates and positions clouds with 1 compound type", + "[microbe]") +{ + setCloudsAndRunInitial( + {Compound{1, "a", true, true, false, Ogre::ColourValue(0, 1, 2, 3)}}); + + // Find the cloud entities + const auto clouds = findClouds(); + + CHECK(clouds.size() == 9); + + // Check that cloud positioning has worked + const auto valid = simpleCloudPosCheck( + clouds, CompoundCloudSystem::calculateGridPositions(Float3(0, 0, 0))); + + for(bool entry : valid) + CHECK(entry); +} + +TEST_CASE_METHOD(CloudManagerTestsFixture, + "Cloud manager creates and positions clouds with 5 compound types", + "[microbe]") +{ + // This test assumes + static_assert(CLOUDS_IN_ONE == 4, "this test assumes this"); + + const std::vector types{ + Compound{1, "a", true, true, false, Ogre::ColourValue(0, 1, 2, 1)}, + Compound{2, "b", true, true, false, Ogre::ColourValue(3, 4, 5, 1)}, + Compound{3, "c", true, true, false, Ogre::ColourValue(6, 7, 8, 1)}, + Compound{4, "d", true, true, false, Ogre::ColourValue(9, 10, 11, 1)}, + Compound{5, "e", true, true, false, Ogre::ColourValue(12, 13, 14, 1)}}; + + std::array cloudFirstTypes; + cloudFirstTypes[0] = types[0].id; + cloudFirstTypes[1] = types[4].id; + + setCloudsAndRunInitial(types); + + // Find the cloud entities + const auto clouds = findClouds(); + + CHECK(clouds.size() == 18); + + multiCloudPositionCheckHelper( + clouds, playerPos->Members._Position, cloudFirstTypes); +} + + +TEST_CASE_METHOD(CloudManagerTestsFixture, + "Cloud manager repositions on player move clouds with 1 compound type", + "[microbe]") +{ + constexpr auto PLAYER_MOVE_AMOUNT = 1000; + + setCloudsAndRunInitial( + {Compound{1, "a", true, true, false, Ogre::ColourValue(0, 1, 2, 3)}}); + + // Find the cloud entities + const auto clouds = findClouds(); + + CHECK(clouds.size() == 9); + + // Check that cloud positioning has worked + auto valid = simpleCloudPosCheck( + clouds, CompoundCloudSystem::calculateGridPositions(Float3(0, 0, 0))); + + for(bool entry : valid) + CHECK(entry); + + // Move player + playerPos->Members._Position.X += PLAYER_MOVE_AMOUNT; + playerPos->Marked = true; + + // And tick + world.Tick(1); + + // And then check that update succeeded + valid = simpleCloudPosCheck( + clouds, CompoundCloudSystem::calculateGridPositions( + CompoundCloudSystem::calculateGridCenterForPlayerPos( + Float3(PLAYER_MOVE_AMOUNT, 0, 0)))); + + for(bool entry : valid) + CHECK(entry); +} + +TEST_CASE_METHOD(CloudManagerTestsFixture, + "Cloud manager repositions on player move clouds with 5 compound types", + "[microbe]") +{ + // This test assumes + static_assert(CLOUDS_IN_ONE == 4, "this test assumes this"); + + const std::vector types{ + Compound{1, "a", true, true, false, Ogre::ColourValue(0, 1, 2, 1)}, + Compound{2, "b", true, true, false, Ogre::ColourValue(3, 4, 5, 1)}, + Compound{3, "c", true, true, false, Ogre::ColourValue(6, 7, 8, 1)}, + Compound{4, "d", true, true, false, Ogre::ColourValue(9, 10, 11, 1)}, + Compound{5, "e", true, true, false, Ogre::ColourValue(12, 13, 14, 1)}}; + + std::array cloudFirstTypes; + cloudFirstTypes[0] = types[0].id; + cloudFirstTypes[1] = types[4].id; + + setCloudsAndRunInitial(types); + + // Find the cloud entities + const auto clouds = findClouds(); + + CHECK(clouds.size() == 18); + + multiCloudPositionCheckHelper( + clouds, playerPos->Members._Position, cloudFirstTypes); + + SECTION("Moving 1000 units") + { + constexpr auto PLAYER_MOVE_AMOUNT = 1000; + + movePlayerXUnits(PLAYER_MOVE_AMOUNT); + + multiCloudPositionCheckHelper( + clouds, playerPos->Members._Position, cloudFirstTypes); + } +} + +TEST_CASE_METHOD(CloudManagerTestsFixture, + "Cloud manager puts spawned cloud in right entity with 5 compound types", + "[microbe]") +{ + // This test assumes + static_assert(CLOUDS_IN_ONE == 4, "this test assumes this"); + + const std::vector types{ + Compound{1, "a", true, true, false, Ogre::ColourValue(0, 1, 2, 1)}, + Compound{2, "b", true, true, false, Ogre::ColourValue(3, 4, 5, 1)}, + Compound{3, "c", true, true, false, Ogre::ColourValue(6, 7, 8, 1)}, + Compound{4, "d", true, true, false, Ogre::ColourValue(9, 10, 11, 1)}, + Compound{5, "e", true, true, false, Ogre::ColourValue(12, 13, 14, 1)}}; + + std::array cloudFirstTypes; + cloudFirstTypes[0] = types[0].id; + cloudFirstTypes[1] = types[4].id; + + setCloudsAndRunInitial(types); + + // Find the cloud entities + const auto clouds = findClouds(); + + CHECK(clouds.size() == 18); + + CompoundCloudComponent* cloudGroup1AtOrigin = nullptr; + CompoundCloudComponent* cloudGroup2AtOrigin = nullptr; + + for(auto* cloud : clouds) { + + if(cloud->getPosition() != Float3(0, 0, 0)) + continue; + + if(cloudFirstTypes[0] == cloud->getCompoundId1()) { + + CHECK(!cloudGroup1AtOrigin); + cloudGroup1AtOrigin = cloud; + + } else if(cloudFirstTypes[1] == cloud->getCompoundId1()) { + + CHECK(!cloudGroup2AtOrigin); + cloudGroup2AtOrigin = cloud; + } + } + + REQUIRE(cloudGroup1AtOrigin); + REQUIRE(cloudGroup2AtOrigin); + + const auto centerCoords = CompoundCloudSystem::convertWorldToCloudLocal( + Float3(0, 0, 0), Float3(0, 0, 0)); + + // Spawn clouds one by one and make sure they went to the right place + world.GetCompoundCloudSystem().addCloud(3, 10, Float3(0, 0, 0)); + + CHECK(cloudGroup1AtOrigin->amountAvailable(3, std::get<0>(centerCoords), + std::get<1>(centerCoords), 1) == 10); + + + world.GetCompoundCloudSystem().addCloud(4, 12, Float3(0, 0, 0)); + + CHECK(cloudGroup1AtOrigin->amountAvailable(4, std::get<0>(centerCoords), + std::get<1>(centerCoords), 1) == 12); + + world.GetCompoundCloudSystem().addCloud(1, 13, Float3(0, 0, 0)); + + CHECK(cloudGroup1AtOrigin->amountAvailable(1, std::get<0>(centerCoords), + std::get<1>(centerCoords), 1) == 13); + + world.GetCompoundCloudSystem().addCloud(2, 14, Float3(0, 0, 0)); + + CHECK(cloudGroup1AtOrigin->amountAvailable(2, std::get<0>(centerCoords), + std::get<1>(centerCoords), 1) == 14); + + // Second group + world.GetCompoundCloudSystem().addCloud(5, 15, Float3(0, 0, 0)); + + CHECK(cloudGroup2AtOrigin->amountAvailable(5, std::get<0>(centerCoords), + std::get<1>(centerCoords), 1) == 15); +} diff --git a/test/test_component.h b/test/test_component.h deleted file mode 100644 index 9a17b44338c..00000000000 --- a/test/test_component.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include "engine/component.h" -#include "engine/serialization.h" -#include "engine/typedefs.h" - -#include - -template class TestComponent : public thrive::Component { - -public: - static const thrive::ComponentTypeId TYPE_ID = ID + 10000; - - thrive::ComponentTypeId - typeId() const override - { - return TYPE_ID; - }; - - static const std::string& - TYPE_NAME() - { - static std::string string = - "TestComponent" + boost::lexical_cast(ID); - return string; - } - - std::string - typeName() const override - { - return TYPE_NAME(); - }; - - void - load(const thrive::StorageContainer& storage) override - { - Component::load(storage); - } - - thrive::StorageContainer - storage() const override - { - return Component::storage(); - } -}; diff --git a/test/test_thrive_game.h b/test/test_thrive_game.h new file mode 100644 index 00000000000..3bdfc364e15 --- /dev/null +++ b/test/test_thrive_game.h @@ -0,0 +1,33 @@ +// Thrive Game +// Copyright (C) 2013-2019 Revolutionary Games +#pragma once +// ------------------------------------ // +#include "ThriveGame.h" + +#include "catch.hpp" + +namespace thrive { namespace test { +//! \brief A test dummy for tests needing ThriveGame +class TestThriveGame : public ThriveGame { +public: + TestThriveGame(Leviathan::Engine* engine) : ThriveGame(engine) + { + // We need to fake key configurations for things + ApplicationConfiguration = new Leviathan::AppDef(true); + ApplicationConfiguration->ReplaceGameAndKeyConfigInMemory( + nullptr, &ThriveGame::CheckGameKeyConfigVariables); + } + + ~TestThriveGame() + { + delete ApplicationConfiguration; + } + + void + lightweightInit() + { + REQUIRE(createImpl()); + } +}; + +}} // namespace thrive::test