diff --git a/assets/definitions/compounds.xml b/assets/definitions/compounds.xml index 17365072f86..6e7be02f470 100644 --- a/assets/definitions/compounds.xml +++ b/assets/definitions/compounds.xml @@ -1,47 +1,47 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/microbe_editor/microbe_editor.lua b/scripts/microbe_editor/microbe_editor.lua index 5da464206a4..43dff195c63 100644 --- a/scripts/microbe_editor/microbe_editor.lua +++ b/scripts/microbe_editor/microbe_editor.lua @@ -19,12 +19,12 @@ function MicrobeEditor:__init(hudSystem) self.gridVisible = true self.mutationPoints = 100 self.placementFunctions = {["nucleus"] = MicrobeEditor.createNewMicrobe, - ["flagelium"] = MicrobeEditor.addMovementOrganelle, - ["mitochondrion"] = MicrobeEditor.addProcessOrganelle, - ["chloroplast"] = MicrobeEditor.addProcessOrganelle, - ["toxin"] = MicrobeEditor.addAgentVacuole, + ["flagellum"] = MicrobeEditor.addOrganelle, + ["mitochondrion"] = MicrobeEditor.addOrganelle, + ["chloroplast"] = MicrobeEditor.addOrganelle, + ["oxytoxy"] = MicrobeEditor.addOrganelle, - ["vacuole"] = MicrobeEditor.addStorageOrganelle, + ["vacuole"] = MicrobeEditor.addOrganelle, -- ["aminosynthesizer"] = MicrobeEditor.addProcessOrganelle, ["remove"] = MicrobeEditor.removeOrganelle} self.actionHistory = nil @@ -54,6 +54,7 @@ function MicrobeEditor:activate() end function MicrobeEditor:update(renderTime, logicTime) + -- self.nextMicrobeEntity being a temporary used to pass the microbe from game to editor if self.nextMicrobeEntity ~= nil then self.currentMicrobe = Microbe(self.nextMicrobeEntity) self.currentMicrobe.sceneNode.transform.orientation = Quaternion(Radian(Degree(180)), Vector3(0, 0, 1))-- Orientation @@ -154,56 +155,14 @@ function MicrobeEditor:getMouseHex() return qr, rr end -function MicrobeEditor:removeOrganelle() - local q, r = self:getMouseHex() - if not (q == 0 and r == 0) then -- Don't remove nucleus - local organelle = self.currentMicrobe:getOrganelleAt(q,r) - if organelle then - local storage = organelle:storage() - self:enqueueAction{ - cost = 10, - redo = function() - self.currentMicrobe:removeOrganelle(q, r) - self.currentMicrobe.sceneNode.transform:touch() - self.organelleCount = self.organelleCount - 1 - end, - undo = function() - self.currentMicrobe:addOrganelle(q, r, Organelle.loadOrganelle(storage)) - self.organelleCount = self.organelleCount + 1 - end - } - end - end -end - - -function MicrobeEditor:addStorageOrganelle(organelleType) - -- self.currentMicrobe = Microbe(Entity("working_microbe", GameState.MICROBE)) - local q, r = self:getMouseHex() - if self.currentMicrobe:getOrganelleAt(q, r) == nil then - self:enqueueAction{ - cost = Organelle.mpCosts["vacuole"], - redo = function() - self.currentMicrobe:addOrganelle(q, r, OrganelleFactory.make_vacuole({})) - self.organelleCount = self.organelleCount + 1 - end, - undo = function() - self.currentMicrobe:removeOrganelle(q, r) - self.currentMicrobe.sceneNode.transform:touch() - self.organelleCount = self.organelleCount - 1 - end - } - end -end - - -function MicrobeEditor:addMovementOrganelle(organelleType) +function MicrobeEditor:addOrganelle(organelleType) local q, r = self:getMouseHex() if self.currentMicrobe:getOrganelleAt(q, r) == nil then - self:enqueueAction{ - cost = Organelle.mpCosts["flagellum"], + local data = {["name"]=organelleType, ["q"]=q, ["r"]=r} + self:enqueueAction({ + cost = Organelle.mpCosts[organelleType], redo = function() - self.currentMicrobe:addOrganelle(q,r, OrganelleFactory.make_flagellum{["q"]=q, ["r"]=r}) + self.currentMicrobe:addOrganelle(q,r, OrganelleFactory.makeOrganelle(data)) self.organelleCount = self.organelleCount + 1 end, undo = function() @@ -211,45 +170,26 @@ function MicrobeEditor:addMovementOrganelle(organelleType) self.currentMicrobe.sceneNode.transform:touch() self.organelleCount = self.organelleCount - 1 end - } + }) end end -function MicrobeEditor:addProcessOrganelle(organelleType) +function MicrobeEditor:removeOrganelle() local q, r = self:getMouseHex() - - if organelleType and self.currentMicrobe:getOrganelleAt(q, r) == nil then - local data = { ["name"] = organelleType } - - self:enqueueAction{ - cost = Organelle.mpCosts[data.name], - redo = function() - self.currentMicrobe:addOrganelle(q, r, OrganelleFactory.makeOrganelle(data)) - self.organelleCount = self.organelleCount + 1 - end, - undo = function() - self.currentMicrobe:removeOrganelle(q, r) - self.currentMicrobe.sceneNode.transform:touch() - self.organelleCount = self.organelleCount - 1 - end - } - end -end - -function MicrobeEditor:addAgentVacuole(organelleType) - if organelleType == "toxin" then - local q, r = self:getMouseHex() - if self.currentMicrobe:getOrganelleAt(q, r) == nil then + if not (q == 0 and r == 0) then -- Don't remove nucleus + local organelle = self.currentMicrobe:getOrganelleAt(q,r) + if organelle then + local storage = organelle:storage() self:enqueueAction{ - cost = Organelle.mpCosts["oxytoxy"], + cost = 10, redo = function() - self.currentMicrobe:addOrganelle(q, r, OrganelleFactory.make_oxytoxy({})) - self.organelleCount = self.organelleCount + 1 - end, - undo = function() self.currentMicrobe:removeOrganelle(q, r) self.currentMicrobe.sceneNode.transform:touch() self.organelleCount = self.organelleCount - 1 + end, + undo = function() + self.currentMicrobe:addOrganelle(q, r, Organelle.loadOrganelle(storage)) + self.organelleCount = self.organelleCount + 1 end } end @@ -257,7 +197,7 @@ function MicrobeEditor:addAgentVacuole(organelleType) end function MicrobeEditor:addNucleus() - local nucleusOrganelle = OrganelleFactory.make_nucleus({}) + local nucleusOrganelle = OrganelleFactory.makeOrganelle({["name"]="nucleus"}) self.currentMicrobe:addOrganelle(0, 0, nucleusOrganelle) end @@ -270,7 +210,6 @@ function MicrobeEditor:loadMicrobe(entityId) self.currentMicrobe.entity:stealName("working_microbe") self.currentMicrobe.sceneNode.transform.orientation = Quaternion(Radian(Degree(180)), Vector3(0, 0, 1))-- Orientation self.currentMicrobe.sceneNode.transform:touch() - self.currentMicrobe.collisionHandler:addCollisionGroup("powerupable") Engine:playerData():setActiveCreature(entityId, GameState.MICROBE_EDITOR) self.mutationPoints = 0 -- resetting the action history - it should not become entangled with the local file system @@ -289,7 +228,6 @@ function MicrobeEditor:createNewMicrobe() self.currentMicrobe.entity:stealName("working_microbe") self.currentMicrobe.sceneNode.transform.orientation = Quaternion(Radian(Degree(180)), Vector3(0, 0, 1))-- Orientation self.currentMicrobe.sceneNode.transform:touch() - self.currentMicrobe.collisionHandler:addCollisionGroup("powerupable") self:addNucleus() self.mutationPoints = 100 Engine:playerData():setActiveCreature(self.currentMicrobe.entity.id, GameState.MICROBE_EDITOR) @@ -310,7 +248,6 @@ function MicrobeEditor:createNewMicrobe() self.currentMicrobe.entity:stealName("working_microbe") self.currentMicrobe.sceneNode.transform.orientation = Quaternion(Radian(Degree(180)), Vector3(0, 0, 1))-- Orientation self.currentMicrobe.sceneNode.transform:touch() - self.currentMicrobe.collisionHandler:addCollisionGroup("powerupable") for position,storage in pairs(organelleStorage) do local q, r = decodeAxial(position) self.currentMicrobe:addOrganelle(q, r, Organelle.loadOrganelle(storage)) diff --git a/scripts/microbe_editor/microbe_editor_hud.lua b/scripts/microbe_editor/microbe_editor_hud.lua index 129cc8061f9..e0bfdd7dfe0 100644 --- a/scripts/microbe_editor/microbe_editor_hud.lua +++ b/scripts/microbe_editor/microbe_editor_hud.lua @@ -34,20 +34,20 @@ function MicrobeEditorHudSystem:init(gameState) -- self.mpProgressBar = root:getChild("BottomSection"):getChild("MutationPoints"):getChild("MPBar") self.organelleScrollPane = root:getChild("scrollablepane"); local nucleusButton = root:getChild("NewMicrobe") - local flageliumButton = root:getChild("scrollablepane"):getChild("AddFlagellum") + local flagellumButton = root:getChild("scrollablepane"):getChild("AddFlagellum") local mitochondriaButton = root:getChild("scrollablepane"):getChild("AddMitochondria") local vacuoleButton = root:getChild("scrollablepane"):getChild("AddVacuole") local toxinButton = root:getChild("scrollablepane"):getChild("AddToxinVacuole") local chloroplastButton = root:getChild("scrollablepane"):getChild("AddChloroplast") self.organelleButtons["nucleus"] = nucleusButton - self.organelleButtons["flagelium"] = flageliumButton + self.organelleButtons["flagellum"] = flagellumButton self.organelleButtons["mitochondrion"] = mitochondriaButton self.organelleButtons["chloroplast"] = chloroplastButton self.organelleButtons["vacuole"] = vacuoleButton self.organelleButtons["Toxin"] = toxinButton self.activeButton = nil nucleusButton:registerEventHandler("Clicked", function() self:nucleusClicked() end) - flageliumButton:registerEventHandler("Clicked", function() self:flageliumClicked() end) + flagellumButton:registerEventHandler("Clicked", function() self:flagellumClicked() end) mitochondriaButton:registerEventHandler("Clicked", function() self:mitochondriaClicked() end) chloroplastButton:registerEventHandler("Clicked", function() self:chloroplastClicked() end) vacuoleButton:registerEventHandler("Clicked", function() self:vacuoleClicked() end) @@ -134,7 +134,7 @@ function MicrobeEditorHudSystem:update(renderTime, logicTime) self.editor:performLocationAction() end elseif keyCombo(kmp.flagellum) then - self:flageliumClicked() + self:flagellumClicked() self.editor:performLocationAction() elseif keyCombo(kmp.mitochondrion) then self:mitochondriaClicked() @@ -241,13 +241,13 @@ function MicrobeEditorHudSystem:nucleusClicked() self:setActiveAction("nucleus") end -function MicrobeEditorHudSystem:flageliumClicked() +function MicrobeEditorHudSystem:flagellumClicked() if self.activeButton ~= nil then self.activeButton:enable() end - self.activeButton = self.organelleButtons["flagelium"] + self.activeButton = self.organelleButtons["flagellum"] self.activeButton:disable() - self:setActiveAction("flagelium") + self:setActiveAction("flagellum") end function MicrobeEditorHudSystem:mitochondriaClicked() @@ -292,7 +292,7 @@ function MicrobeEditorHudSystem:toxinClicked() end self.activeButton = self.organelleButtons["Toxin"] self.activeButton:disable() - self:setActiveAction("toxin") + self:setActiveAction("oxytoxy") end diff --git a/scripts/microbe_stage/manifest.txt b/scripts/microbe_stage/manifest.txt index f5bed3db272..04cc0e237ee 100644 --- a/scripts/microbe_stage/manifest.txt +++ b/scripts/microbe_stage/manifest.txt @@ -6,6 +6,8 @@ microbe.lua camera.lua microbe_control.lua spawn_system.lua +species_system.lua +patch_system.lua switch_game_state_system.lua // Organelles diff --git a/scripts/microbe_stage/microbe.lua b/scripts/microbe_stage/microbe.lua index 88cd10054d5..5430020ddb5 100644 --- a/scripts/microbe_stage/microbe.lua +++ b/scripts/microbe_stage/microbe.lua @@ -17,9 +17,9 @@ REPRODUCTASE_TO_SPLIT = 5 RELATIVE_VELOCITY_TO_BUMP_SOUND = 6 INITIAL_EMISSION_RADIUS = 2 -function MicrobeComponent:__init(isPlayerMicrobe) +function MicrobeComponent:__init(isPlayerMicrobe, speciesName) Component.__init(self) - self.speciesName = "Default" + self.speciesName = speciesName self.hitpoints = 10 self.maxHitpoints = 10 self.dead = false @@ -140,6 +140,7 @@ function MicrobeComponent:load(storage) self.maxBandwidth = storage:get("maxBandwidth", 0) self.remainingBandwidth = storage:get("remainingBandwidth", 0) self.isPlayerMicrobe = storage:get("isPlayerMicrobe", false) + self.speciesName = storage:get("speciesName", "") local storedCompound = storage:get("storedCompounds", {}) for i = 1,storedCompound:size() do local compound = storedCompound:get(i) @@ -171,6 +172,7 @@ function MicrobeComponent:storage() storage:set("remainingBandwidth", self.remainingBandwidth) storage:set("maxBandwidth", self.maxBandwidth) storage:set("isPlayerMicrobe", self.isPlayerMicrobe) + storage:set("speciesName", self.speciesName) local storedCompounds = StorageList() for compoundId, amount in pairs(self.compounds) do compound = StorageContainer() @@ -208,7 +210,7 @@ class 'Microbe' -- -- @returns microbe -- An object of type Microbe -function Microbe.createMicrobeEntity(name, aiControlled) +function Microbe.createMicrobeEntity(name, aiControlled, speciesName) local entity if name then entity = Entity(name) @@ -247,7 +249,7 @@ function Microbe.createMicrobeEntity(name, aiControlled) local components = { CompoundAbsorberComponent(), OgreSceneNodeComponent(), - MicrobeComponent(not aiControlled), + MicrobeComponent(not aiControlled, speciesName), reactionHandler, rigidBody, compoundEmitter, @@ -301,6 +303,12 @@ function Microbe:__init(entity) self.playerAlreadyShownVictory = false end +-- Getter for microbe species +-- +-- returns the species component or nil if it doesn't have a valid species +function Microbe:getSpeciesComponent() + return Entity(self.microbe.speciesName):getComponent(SpeciesComponent.TYPE_ID) +end -- Adds a new organelle -- @@ -690,22 +698,20 @@ function Microbe:kill() self.microbe.movementDirection = Vector3(0,0,0) self.rigidBody:clearForces() microbeSceneNode.visible = false + --[[ since other microbes can kll each other now, this is deprecated if self.microbe.isPlayerMicrobe ~= true then if not self.playerAlreadyShownVictory then self.playerAlreadyShownVictory = true showMessage("VICTORY!!!") end end + --]] end --- Copies this microbe. The new microbe will not have the stored compounds of this one. +-- Copies this microbe. The new microbe will not have the stored compounds of this one. function Microbe:reproduce() copy = Microbe.createMicrobeEntity(nil, true) - for _, organelle in pairs(self.microbe.organelles) do - local organelleStorage = organelle:storage() - local organelle = Organelle.loadOrganelle(organelleStorage) - copy:addOrganelle(organelle.position.q, organelle.position.r, organelle) - end + self:getSpeciesComponent():template(copy) copy.rigidBody.dynamicProperties.position = Vector3(self.rigidBody.dynamicProperties.position.x, self.rigidBody.dynamicProperties.position.y, 0) copy:storeCompound(CompoundRegistry.getCompoundId("atp"), 20, false) copy.microbe:_resetCompoundPriorities() @@ -715,7 +721,6 @@ function Microbe:reproduce() end end - -- Updates the microbe's state function Microbe:update(logicTime) if not self.microbe.dead then @@ -736,90 +741,113 @@ function Microbe:update(logicTime) end self.microbe.compoundCollectionTimer = self.microbe.compoundCollectionTimer + logicTime - while self.microbe.compoundCollectionTimer > EXCESS_COMPOUND_COLLECTION_INTERVAL do -- For every COMPOUND_DISTRIBUTION_INTERVAL passed - -- Gather excess compounds that are the compounds that the storage organelles automatically emit to stay less than full - local excessCompounds = {} - while self.microbe.stored/self.microbe.capacity > STORAGE_EJECTION_THRESHHOLD+0.01 do - -- Find lowest priority compound type contained in the microbe - local lowestPriorityId = nil - local lowestPriority = math.huge - for compoundId,_ in pairs(self.microbe.compounds) do - assert(self.microbe.compoundPriorities[compoundId] ~= nil, "Compound priority table was missing compound") - if self.microbe.compounds[compoundId] > 0 and self.microbe.compoundPriorities[compoundId] < lowestPriority then - lowestPriority = self.microbe.compoundPriorities[compoundId] - lowestPriorityId = compoundId - end - end - assert(lowestPriorityId ~= nil, "The microbe didn't seem to contain any compounds but was over the threshold") - assert(self.microbe.compounds[lowestPriorityId] ~= nil, "Microbe storage was over threshold but didn't have any valid compounds to expell") - -- Return an amount that either is how much the microbe contains of the compound or until it goes to the threshhold - local amountInExcess - - amountInExcess = math.min(self.microbe.compounds[lowestPriorityId],self.microbe.stored - self.microbe.capacity * STORAGE_EJECTION_THRESHHOLD) - excessCompounds[lowestPriorityId] = self:takeCompound(lowestPriorityId, amountInExcess) - end - -- Expell compounds of priority 0 periodically - for compoundId,_ in pairs(self.microbe.compounds) do - if self.microbe.compoundPriorities[compoundId] == 0 and self.microbe.compounds[compoundId] > 1 then - local uselessCompoundAmount - uselessCompoundAmount = self.microbe:getBandwidth(self.microbe.compounds[compoundId], compoundId) - if excessCompounds[compoundId] ~= nil then - excessCompounds[compoundId] = excessCompounds[compoundId] + self:takeCompound(compoundId, uselessCompoundAmount) - else - excessCompounds[compoundId] = self:takeCompound(compoundId, uselessCompoundAmount) - end - end - end - for compoundId, amount in pairs(excessCompounds) do - if amount > 0 then - self:ejectCompound(compoundId, amount, 160, 200) - end - end - -- Damage microbe if its too low on ATP - if self.microbe.compounds[CompoundRegistry.getCompoundId("atp")] ~= nil and self.microbe.compounds[CompoundRegistry.getCompoundId("atp")] < 1.0 then - if self.microbe.isPlayerMicrobe and not self.playerAlreadyShownAtpDamage then - self.playerAlreadyShownAtpDamage = true - showMessage("No ATP hurts you!") - end - self:damage(EXCESS_COMPOUND_COLLECTION_INTERVAL * 0.00002 * self.microbe.maxHitpoints) -- Microbe takes 2% of max hp per second in damage - end - -- Split microbe if it has enough reproductase - if self.microbe.compounds[CompoundRegistry.getCompoundId("reproductase")] ~= nil and self.microbe.compounds[CompoundRegistry.getCompoundId("reproductase")] > REPRODUCTASE_TO_SPLIT then - self:takeCompound(CompoundRegistry.getCompoundId("reproductase"), 5) - self:reproduce() - end + while self.microbe.compoundCollectionTimer > EXCESS_COMPOUND_COLLECTION_INTERVAL do + -- For every COMPOUND_DISTRIBUTION_INTERVAL passed + self.microbe.compoundCollectionTimer = self.microbe.compoundCollectionTimer - EXCESS_COMPOUND_COLLECTION_INTERVAL + + self:purgeCompounds() + + self:atpDamage() + + self:attemptReproduce() end + -- Other organelles for _, organelle in pairs(self.microbe.organelles) do organelle:update(self, logicTime) end + + self.compoundAbsorber:setAbsorbtionCapacity(self.microbe.remainingBandwidth) else self.microbe.deathTimer = self.microbe.deathTimer - logicTime if self.microbe.deathTimer <= 0 then if self.microbe.isPlayerMicrobe == true then - self.microbe.dead = false - self.microbe.deathTimer = 0 - self.residuePhysicsTime = 0 - self.microbe.hitpoints = self.microbe.maxHitpoints - - self.rigidBody:setDynamicProperties( - Vector3(0,0,0), -- Position - Quaternion(Radian(Degree(0)), Vector3(1, 0, 0)), -- Orientation - Vector3(0, 0, 0), -- Linear velocity - Vector3(0, 0, 0) -- Angular velocity - ) - local sceneNode = self.entity:getComponent(OgreSceneNodeComponent.TYPE_ID) - sceneNode.visible = true - self:storeCompound(CompoundRegistry.getCompoundId("atp"), 20, false) + self:respawn() else self:destroy() end end end - self.compoundAbsorber:setAbsorbtionCapacity(self.microbe.remainingBandwidth) end +function Microbe:purgeCompounds() + -- Gather excess compounds that are the compounds that the storage organelles automatically emit to stay less than full + local excessCompounds = {} + while self.microbe.stored/self.microbe.capacity > STORAGE_EJECTION_THRESHHOLD+0.01 do + -- Find lowest priority compound type contained in the microbe + local lowestPriorityId = nil + local lowestPriority = math.huge + for compoundId,_ in pairs(self.microbe.compounds) do + assert(self.microbe.compoundPriorities[compoundId] ~= nil, "Compound priority table was missing compound") + if self.microbe.compounds[compoundId] > 0 and self.microbe.compoundPriorities[compoundId] < lowestPriority then + lowestPriority = self.microbe.compoundPriorities[compoundId] + lowestPriorityId = compoundId + end + end + assert(lowestPriorityId ~= nil, "The microbe didn't seem to contain any compounds but was over the threshold") + assert(self.microbe.compounds[lowestPriorityId] ~= nil, "Microbe storage was over threshold but didn't have any valid compounds to expell") + -- Return an amount that either is how much the microbe contains of the compound or until it goes to the threshhold + local amountInExcess + + amountInExcess = math.min(self.microbe.compounds[lowestPriorityId],self.microbe.stored - self.microbe.capacity * STORAGE_EJECTION_THRESHHOLD) + excessCompounds[lowestPriorityId] = self:takeCompound(lowestPriorityId, amountInExcess) + end + + -- Expel compounds of priority 0 periodically + for compoundId,_ in pairs(self.microbe.compounds) do + if self.microbe.compoundPriorities[compoundId] == 0 and self.microbe.compounds[compoundId] > 1 then + local uselessCompoundAmount + uselessCompoundAmount = self.microbe:getBandwidth(self.microbe.compounds[compoundId], compoundId) + if excessCompounds[compoundId] ~= nil then + excessCompounds[compoundId] = excessCompounds[compoundId] + self:takeCompound(compoundId, uselessCompoundAmount) + else + excessCompounds[compoundId] = self:takeCompound(compoundId, uselessCompoundAmount) + end + end + end + for compoundId, amount in pairs(excessCompounds) do + if amount > 0 then + self:ejectCompound(compoundId, amount, 160, 200, true) + end + end +end + +function Microbe:atpDamage() + -- Damage microbe if its too low on ATP + if self.microbe.compounds[CompoundRegistry.getCompoundId("atp")] ~= nil and self.microbe.compounds[CompoundRegistry.getCompoundId("atp")] < 1.0 then + if self.microbe.isPlayerMicrobe and not self.playerAlreadyShownAtpDamage then + self.playerAlreadyShownAtpDamage = true + showMessage("No ATP hurts you!") + end + self:damage(EXCESS_COMPOUND_COLLECTION_INTERVAL * 0.00002 * self.microbe.maxHitpoints) -- Microbe takes 2% of max hp per second in damage + end +end + +function Microbe:attemptReproduce() + -- Split microbe if it has enough reproductase + if self.microbe.compounds[CompoundRegistry.getCompoundId("reproductase")] ~= nil and self.microbe.compounds[CompoundRegistry.getCompoundId("reproductase")] > REPRODUCTASE_TO_SPLIT then + self:takeCompound(CompoundRegistry.getCompoundId("reproductase"), 5) + self:reproduce() + end +end + +function Microbe:respawn() + self.microbe.dead = false + self.microbe.deathTimer = 0 + self.residuePhysicsTime = 0 + self.microbe.hitpoints = self.microbe.maxHitpoints + + self.rigidBody:setDynamicProperties( + Vector3(0,0,0), -- Position + Quaternion(Radian(Degree(0)), Vector3(1, 0, 0)), -- Orientation + Vector3(0, 0, 0), -- Linear velocity + Vector3(0, 0, 0) -- Angular velocity + ) + local sceneNode = self.entity:getComponent(OgreSceneNodeComponent.TYPE_ID) + sceneNode.visible = true + self:storeCompound(CompoundRegistry.getCompoundId("atp"), 20, false) +end -- Private function for initializing a microbe's components function Microbe:_initialize() diff --git a/scripts/microbe_stage/microbe_replacement.lua b/scripts/microbe_stage/microbe_replacement.lua index 9f8e49eccef..ac8f72bc2b4 100644 --- a/scripts/microbe_stage/microbe_replacement.lua +++ b/scripts/microbe_stage/microbe_replacement.lua @@ -5,22 +5,37 @@ class 'MicrobeReplacementSystem' (System) global_newEditorMicrobe = false function MicrobeReplacementSystem:__init() + self.globalSpeciesNameCounter = 1 System.__init(self) end function MicrobeReplacementSystem:activate() activeCreatureId = Engine:playerData():activeCreature() if Engine:playerData():isBoolSet("edited_microbe") then - Engine:playerData():setBool("edited_microbe", false); + Engine:playerData():setBool("edited_microbe", false) + workingMicrobe = Microbe(Entity(activeCreatureId, GameState.MICROBE_EDITOR)) - if workingMicrobe:getCompoundAmount(CompoundRegistry.getCompoundId("atp")) == 0 then - workingMicrobe:storeCompound(CompoundRegistry.getCompoundId("atp"), 10) + + speciesEntity = Entity(workingMicrobe.microbe.speciesName, GameState.MICROBE) + species = SpeciesComponent(workingMicrobe.microbe.speciesName) + speciesEntity:addComponent(species) + self.globalSpeciesNameCounter = self.globalSpeciesNameCounter + 1 + species:fromMicrobe(workingMicrobe) + + workingMicrobe.entity:destroy() + + newMicrobe = Microbe.createMicrobeEntity(PLAYER_NAME, false, workingMicrobe.microbe.speciesName) + species:template(newMicrobe) + + if newMicrobe:getCompoundAmount(CompoundRegistry.getCompoundId("atp")) < 10 then + newMicrobe:storeCompound(CompoundRegistry.getCompoundId("atp"), 10) end - newMicrobeEntity = workingMicrobe.entity - newPlayerMicrobe = newMicrobeEntity:transfer(GameState.MICROBE) - newPlayerMicrobe:stealName(PLAYER_NAME) + + newMicrobe.collisionHandler:addCollisionGroup("powerupable") + newMicrobeEntity = newMicrobe.entity:transfer(GameState.MICROBE) + newMicrobeEntity:stealName(PLAYER_NAME) global_newEditorMicrobe = false - Engine:playerData():setActiveCreature(newPlayerMicrobe.id, GameState.MICROBE) + Engine:playerData():setActiveCreature(newMicrobeEntity.id, GameState.MICROBE) end end diff --git a/scripts/microbe_stage/microbe_stage_hud.lua b/scripts/microbe_stage/microbe_stage_hud.lua index 5adce778f77..e0a13fc4364 100644 --- a/scripts/microbe_stage/microbe_stage_hud.lua +++ b/scripts/microbe_stage/microbe_stage_hud.lua @@ -8,6 +8,8 @@ function HudSystem:__init() self.hitpointsCountLabel = nil self.hitpointsBar = nil self.compoundListItems = {} + self.rootGuiWindow = nil + self.populationNumberLabel = nil self.rootGUIWindow = nil end @@ -63,7 +65,8 @@ function HudSystem:update(renderTime) self.hitpointsBar:progressbarSetProgress(playerMicrobe.microbe.hitpoints/playerMicrobe.microbe.maxHitpoints) self.hitpointsCountLabel:setText("".. math.floor(playerMicrobe.microbe.hitpoints)) - + local playerSpecies = playerMicrobe:getSpeciesComponent() + --TODO display population in home patch here for compoundID in CompoundRegistry.getCompoundList() do local compoundsString = string.format("%s - %d", CompoundRegistry.getCompoundDisplayName(compoundID), playerMicrobe:getCompoundAmount(compoundID)) if self.compoundListItems[compoundID] == nil then diff --git a/scripts/microbe_stage/movement_organelle.lua b/scripts/microbe_stage/movement_organelle.lua index c0a79ae84c6..ecb71acac1d 100644 --- a/scripts/microbe_stage/movement_organelle.lua +++ b/scripts/microbe_stage/movement_organelle.lua @@ -13,6 +13,7 @@ function MovementOrganelle:__init(force, torque) self.energyMultiplier = 0.025 self.force = force self.torque = torque + self.backwards_multiplier = 0 end function MovementOrganelle:onAddedToMicrobe(microbe, q, r) diff --git a/scripts/microbe_stage/organelle.lua b/scripts/microbe_stage/organelle.lua index 9b0fff433f0..b425c8a4641 100644 --- a/scripts/microbe_stage/organelle.lua +++ b/scripts/microbe_stage/organelle.lua @@ -28,6 +28,7 @@ function Organelle:__init() self._internalEdgeColour = ColourValue(0.5, 0.5, 0.5, 1) self._externalEdgeColour = ColourValue(0, 0, 0, 1) self._needsColourUpdate = false + self.name = "" end @@ -97,6 +98,7 @@ function Organelle:load(storage) self._colour = storage:get("colour", ColourValue.White) self._internalEdgeColour = storage:get("internalEdgeColour", ColourValue.Grey) self._externalEdgeColour = storage:get("externalEdgeColour", ColourValue.Black) + self.name = storage:get("name", "") end @@ -181,6 +183,7 @@ function Organelle:storage() hexes:append(hexStorage) end storage:set("hexes", hexes) + storage:set("name", self.name) storage:set("q", self.position.q) storage:set("r", self.position.r) storage:set("colour", self._colour) @@ -260,6 +263,7 @@ function OrganelleFactory.makeOrganelle(data) end local success, organelle = pcall(make_organelle) if success then + organelle.name = data.name return organelle else if data.name == "" or data.name == nil then data.name = "" end diff --git a/scripts/microbe_stage/patch_system.lua b/scripts/microbe_stage/patch_system.lua new file mode 100644 index 00000000000..74d3d6950d6 --- /dev/null +++ b/scripts/microbe_stage/patch_system.lua @@ -0,0 +1,114 @@ +-------------------------------------------------------------------------------- +-- PatchComponent +-- +-- Controls population management within a certain region of space +-------------------------------------------------------------------------------- + +class "PatchComponent" (Component) + +function PatchComponent:__init() + Component.__init(self) + -- map species to populations + -- model environment +end + +--[[ +We need a bunch of stuff handled here: +Compound movement: +- environment -> species +- species -> environment +- predation: species -> other species and environment + +Population changes: +- immigration -- add tentative populations, keep those viable enough to displace + another species (and give them resources of species displaced, with leakage) +- extinction, by culling +- speciation? We may need to slightly control here when it happens + +Culling: +- when population drops below sustainable level, free all compounds +- when population mutates, and criteria for branching not met, move compounds + from ancestor to new, with leakage +- another case? + +--]] + +REGISTER_COMPONENT("PatchComponent", PatchComponent) + +-------------------------------------------------------------------------------- +-- Population +-- +-- Holds information about a specific population (species \intersect patch) +-------------------------------------------------------------------------------- +class 'Population' + +function Population:__init(species) + self.species = species + self.heldCompounds = {} -- compounds that are available for intracellular processes + self.lockedCompounds = {} -- compounds that aren't, but will be released on deaths +end + +--[[ +Whatever population calculations the patch does that would be useful to factor out will go here + +Getting the effective population number from the lockedCompounds pool is a bit complicated +- each organelle will need an amortized cost, in locked compounds (protein, lipids, polysaccharides) +- SpeciesComponent should calculate average organism cost +- divide out current locked pool by per-unit cost, pick lowest +- fudge +--]] + +-------------------------------------------------------------------------------- +-- PatchSystem +-- +-- System for simulating populations of species and their spatial distributions +-------------------------------------------------------------------------------- + +class "PatchSystem" (System) + +PATCH_SIM_INTERVAL = 1200 + +function PatchSystem:__init() + System.__init(self) + + self.entities = EntityFilter( + { + PatchComponent + }, + true + ) + self.timeSinceLastCycle = 0 +end + + +-- Override from System +function PatchSystem:init(gameState) + System.init(self, gameState) + self.entities:init(gameState) +end + +-- Override from System +function PatchSystem:shutdown() + self.entities:shutdown() + System.shutdown(self) +end + +-- Override from System +function PatchSystem:activate() + --[[ + This handles two (three?) cases: + - First, it runs on game entry from main menu -- orchestrate with rest of world-generation + - Second, game entry from editor -- split patches, + move patches, merge patches + - Third (possibly) it might run on load. + --]] +end + +-- Override from System +function PatchSystem:update(_, milliseconds) + self.timeSinceLastCycle = self.timeSinceLastCycle + milliseconds + while self.timeSinceLastCycle > SPECIES_SIM_INTERVAL do + -- do population-management here + self.timeSinceLastCycle = self.timeSinceLastCycle - PATCH_SIM_INTERVAL + end +end diff --git a/scripts/microbe_stage/setup.lua b/scripts/microbe_stage/setup.lua index be3c30fbf84..4a897bf4796 100644 --- a/scripts/microbe_stage/setup.lua +++ b/scripts/microbe_stage/setup.lua @@ -34,6 +34,7 @@ local function setupCamera() viewportEntity:addComponent(viewportComponent) end +-- there must be some more robust way to script agents than having stuff all over the place. function oxytoxyEffect(entityId, potency) Microbe(Entity(entityId)):damage(potency*15, "toxin") end @@ -66,6 +67,45 @@ local function setupProcesses() end end +function setupSpecies() + --[[ + This function should be the entry point for all initial-species generation + For now, it can go through the XML and instantiate all the species, but later this + would be all procedural. + + Together with the mutate function, these would be the only ways species are created + ]] + SpeciesRegistry.loadFromXML("../definitions/microbes.xml") + for _, name in ipairs(SpeciesRegistry.getSpeciesNames()) do + speciesEntity = Entity(name) + speciesComponent = SpeciesComponent(name) + speciesEntity:addComponent(speciesComponent) + -- print("made entity and component") + local organelles = {} + assert(pcall(function () SpeciesRegistry.getSize(name) end), "could not load species", name, "from XML") + -- In this case the species is a default one loaded from xml + -- print("loaded", name) + local numOrganelles = SpeciesRegistry.getSize(name) + -- print("# organelles = "..numOrganelles) + for i = 0,(numOrganelles-1) do + -- returns a property table + local organelleData = SpeciesRegistry.getOrganelle(name, i) + organelles[#organelles+1] = organelleData + end + -- for _, org in pairs(organelles) do print(org.name, org.q, org.r) end + speciesComponent.organelles = organelles + + -- iterates over all compounds, and sets amounts and priorities + for compoundID in CompoundRegistry.getCompoundList() do + compound = CompoundRegistry.getCompoundInternalName(compoundID) + amount = SpeciesRegistry.getCompoundAmount(name, compound) + priority = SpeciesRegistry.getCompoundPriority(name, compound) + speciesComponent.avgCompoundAmounts[compoundID] = amount + speciesComponent.compoundPriorities[compoundID] = priority + end + end +end + -- speciesName decides the template to use, while individualName is used for referencing the instance function microbeSpawnFunctionGeneric(pos, speciesName, aiControlled, individualName) local microbe = Microbe.createMicrobeEntity(individualName, aiControlled) @@ -77,192 +117,103 @@ function microbeSpawnFunctionGeneric(pos, speciesName, aiControlled, individualN Vector3(0, 0, 0) -- Angular velocity ) end - local numOrganelles = SpeciesRegistry.getSize(speciesName) - for i = 0,(numOrganelles-1) do - -- TODO make a SpeciesRegistry::getOrganelle function that returns a property table - local organelleData = SpeciesRegistry.getOrganelle(speciesName, i) - local organelle = OrganelleFactory.makeOrganelle(organelleData) - microbe:addOrganelle(organelleData.q, organelleData.r, organelle) - end - -- TODO figure out way to iterate this over all compounds, and set priorities too - for compoundID in CompoundRegistry.getCompoundList() do - compound = CompoundRegistry.getCompoundInternalName(compoundID) - amount = SpeciesRegistry.getCompoundAmount(speciesName, compound) - if amount ~= 0 then - microbe:storeCompound(compoundID, amount, false) - end - priority = SpeciesRegistry.getCompoundPriority(speciesName, compound) - if priority ~= 0 then - microbe:setDefaultCompoundPriority(compoundID, priority) - end - end - microbe.microbe.speciesName = speciesName + -- set organelles, starting compound amounts, all that + -- TODO: + Entity(speciesName):getComponent(SpeciesComponent.TYPE_ID):template(microbe) return microbe end +local function setSpawnablePhysics(entity, pos, mesh, scale, collisionShape) + -- Rigid body + local rigidBody = RigidBodyComponent() + rigidBody.properties.friction = 0.2 + rigidBody.properties.linearDamping = 0.8 + + rigidBody.properties.shape = collisionShape + rigidBody:setDynamicProperties( + pos, + Quaternion(Radian(Degree(math.random()*360)), Vector3(0, 0, 1)), + Vector3(0, 0, 0), + Vector3(0, 0, 0) + ) + rigidBody.properties:touch() + entity:addComponent(rigidBody) + -- Scene node + local sceneNode = OgreSceneNodeComponent() + sceneNode.meshName = mesh + sceneNode.transform.scale = Vector3(scale, scale, scale) + entity:addComponent(sceneNode) + return entity +end + +local function addEmitter2Entity(entity, compound) + local compoundEmitter = CompoundEmitterComponent() + entity:addComponent(compoundEmitter) + compoundEmitter.emissionRadius = 1 + compoundEmitter.maxInitialSpeed = 10 + compoundEmitter.minInitialSpeed = 2 + compoundEmitter.minEmissionAngle = Degree(0) + compoundEmitter.maxEmissionAngle = Degree(360) + compoundEmitter.particleLifeTime = 5000 + local timedEmitter = TimedCompoundEmitterComponent() + timedEmitter.compoundId = CompoundRegistry.getCompoundId(compound) + timedEmitter.particlesPerEmission = 1 + timedEmitter.potencyPerParticle = 2.0 + timedEmitter.emitInterval = 1000 + entity:addComponent(timedEmitter) +end + local function createSpawnSystem() local spawnSystem = SpawnSystem() SpeciesRegistry.loadFromXML("../definitions/microbes.xml") local spawnOxygenEmitter = function(pos) - -- Setting up an emitter for oxygen local entity = Entity() - -- Rigid body - local rigidBody = RigidBodyComponent() - rigidBody.properties.friction = 0.2 - rigidBody.properties.linearDamping = 0.8 - rigidBody.properties.shape = CylinderShape( - CollisionShape.AXIS_X, - 0.4, - 2.0 - ) - rigidBody:setDynamicProperties( - pos, - Quaternion(Radian(Degree(math.random()*360)), Vector3(0, 0, 1)), - Vector3(0, 0, 0), - Vector3(0, 0, 0) - ) - rigidBody.properties:touch() - entity:addComponent(rigidBody) - -- Scene node - local sceneNode = OgreSceneNodeComponent() - sceneNode.meshName = "molecule.mesh" - entity:addComponent(sceneNode) - -- Emitter oxygen - local oxygenEmitter = CompoundEmitterComponent() - entity:addComponent(oxygenEmitter) - oxygenEmitter.emissionRadius = 1 - oxygenEmitter.maxInitialSpeed = 10 - oxygenEmitter.minInitialSpeed = 2 - oxygenEmitter.minEmissionAngle = Degree(0) - oxygenEmitter.maxEmissionAngle = Degree(360) - oxygenEmitter.particleLifeTime = 5000 - local timedEmitter = TimedCompoundEmitterComponent() - timedEmitter.compoundId = CompoundRegistry.getCompoundId("oxygen") - timedEmitter.particlesPerEmission = 1 - timedEmitter.potencyPerParticle = 2.0 - timedEmitter.emitInterval = 1000 - entity:addComponent(timedEmitter) + setSpawnablePhysics(entity, pos, "molecule.mesh", 1, CylinderShape( + CollisionShape.AXIS_X, + 0.4, + 2.0 + )) + addEmitter2Entity(entity, "oxygen") return entity end local spawnCO2Emitter = function(pos) - -- Setting up an emitter for co2 local entity = Entity() - -- Rigid body - local rigidBody = RigidBodyComponent() - rigidBody.properties.friction = 0.2 - rigidBody.properties.linearDamping = 0.8 - rigidBody.properties.shape = CylinderShape( - CollisionShape.AXIS_X, - 0.4, - 2.0 - ) - rigidBody:setDynamicProperties( - pos, - Quaternion(Radian(Degree(math.random()*360)), Vector3(0, 0, 1)), - Vector3(0, 0, 0), - Vector3(0, 0, 0) - ) - rigidBody.properties:touch() - entity:addComponent(rigidBody) - -- Scene node - local sceneNode = OgreSceneNodeComponent() - sceneNode.meshName = "co2.mesh" - sceneNode.transform.scale = Vector3(0.4, 0.4, 0.4) - entity:addComponent(sceneNode) - -- Emitter carbon dioxide - local co2Emitter = CompoundEmitterComponent() - entity:addComponent(co2Emitter) - co2Emitter.emissionRadius = 1 - co2Emitter.maxInitialSpeed = 10 - co2Emitter.minInitialSpeed = 2 - co2Emitter.minEmissionAngle = Degree(0) - co2Emitter.maxEmissionAngle = Degree(360) - co2Emitter.particleLifeTime = 5000 - local timedEmitter = TimedCompoundEmitterComponent() - timedEmitter.compoundId = CompoundRegistry.getCompoundId("co2") - timedEmitter.particlesPerEmission = 1 - timedEmitter.potencyPerParticle = 2.0 - timedEmitter.emitInterval = 1000 - entity:addComponent(timedEmitter) + setSpawnablePhysics(entity, pos, "co2.mesh", 0.4, CylinderShape( + CollisionShape.AXIS_X, + 0.4, + 2.0 + )) + addEmitter2Entity(entity, "co2") return entity end local spawnGlucoseEmitter = function(pos) - -- Setting up an emitter for glucose local entity = Entity() - -- Rigid body - local rigidBody = RigidBodyComponent() - rigidBody.properties.friction = 0.2 - rigidBody.properties.linearDamping = 0.8 - rigidBody.properties.shape = SphereShape(HEX_SIZE) - rigidBody:setDynamicProperties( - pos, - Quaternion(Radian(Degree(math.random()*360)), Vector3(0, 0, 1)), - Vector3(0, 0, 0), - Vector3(0, 0, 0) - ) - rigidBody.properties:touch() - entity:addComponent(rigidBody) - -- Scene node - local sceneNode = OgreSceneNodeComponent() - sceneNode.meshName = "glucose.mesh" - entity:addComponent(sceneNode) - -- Emitter glucose - local glucoseEmitter = CompoundEmitterComponent() - entity:addComponent(glucoseEmitter) - glucoseEmitter.emissionRadius = 1 - glucoseEmitter.maxInitialSpeed = 10 - glucoseEmitter.minInitialSpeed = 2 - glucoseEmitter.minEmissionAngle = Degree(0) - glucoseEmitter.maxEmissionAngle = Degree(360) - glucoseEmitter.particleLifeTime = 5000 - local timedEmitter = TimedCompoundEmitterComponent() - timedEmitter.compoundId = CompoundRegistry.getCompoundId("glucose") - timedEmitter.particlesPerEmission = 1 - timedEmitter.potencyPerParticle = 1.0 - timedEmitter.emitInterval = 2000 - entity:addComponent(timedEmitter) + setSpawnablePhysics(entity, pos, "glucose.mesh", 1, SphereShape(HEX_SIZE)) + addEmitter2Entity(entity, "glucose") return entity end local spawnAmmoniaEmitter = function(pos) - -- Setting up an emitter for ammonia local entity = Entity() - -- Rigid body - local rigidBody = RigidBodyComponent() - rigidBody.properties.friction = 0.2 - rigidBody.properties.linearDamping = 0.8 - rigidBody.properties.shape = SphereShape(HEX_SIZE) - rigidBody:setDynamicProperties( - pos, - Quaternion(Radian(Degree(math.random()*360)), Vector3(0, 0, 1)), - Vector3(0, 0, 0), - Vector3(0, 0, 0) - ) - rigidBody.properties:touch() - entity:addComponent(rigidBody) - -- Scene node - local sceneNode = OgreSceneNodeComponent() - sceneNode.meshName = "ammonia.mesh" - sceneNode.transform.scale = Vector3(0.5, 0.5, 0.5) - entity:addComponent(sceneNode) - -- Emitter ammonia - local ammoniaEmitter = CompoundEmitterComponent() - entity:addComponent(ammoniaEmitter) - ammoniaEmitter.emissionRadius = 1 - ammoniaEmitter.maxInitialSpeed = 10 - ammoniaEmitter.minInitialSpeed = 2 - ammoniaEmitter.minEmissionAngle = Degree(0) - ammoniaEmitter.maxEmissionAngle = Degree(360) - ammoniaEmitter.particleLifeTime = 5000 - local timedEmitter = TimedCompoundEmitterComponent() - timedEmitter.compoundId = CompoundRegistry.getCompoundId("ammonia") - timedEmitter.particlesPerEmission = 1 - timedEmitter.potencyPerParticle = 1.0 - timedEmitter.emitInterval = 1000 - entity:addComponent(timedEmitter) + setSpawnablePhysics(entity, pos, "ammonia.mesh", 0.5, SphereShape(HEX_SIZE)) + addEmitter2Entity(entity, "ammonia") return entity end + local toxinOrganelleSpawnFunction = function(pos) + powerupEntity = Entity() + setSpawnablePhysics(powerupEntity, pos, "AgentVacuole.mesh", 0.9, SphereShape(HEX_SIZE)) + + local reactionHandler = CollisionComponent() + reactionHandler:addCollisionGroup("powerup") + powerupEntity:addComponent(reactionHandler) + + local powerupComponent = PowerupComponent() + powerupComponent:setEffect(unlockToxin) + powerupEntity:addComponent(powerupComponent) + return powerupEntity + end + local microbeDefault = function(pos) return microbeSpawnFunctionGeneric(pos, "Default", true, nil) end @@ -282,6 +233,7 @@ local function createSpawnSystem() local microbeToxinPredator = function(pos) return microbeSpawnFunctionGeneric(pos, "ToxinPredator", true, nil) end + local microbeNoper = function(pos) return microbeSpawnFunctionGeneric(pos, "Noper", true, nil) end @@ -328,6 +280,8 @@ local function createSpawnSystem() spawnSystem:addSpawnType(spawnCO2Emitter, 1/500, 30) spawnSystem:addSpawnType(spawnGlucoseEmitter, 1/500, 30) spawnSystem:addSpawnType(spawnAmmoniaEmitter, 1/1250, 30) + + -- Microbe spawning needs to be handled by species/population spawnSystem:addSpawnType(microbeDefault, 1/12000, 40) spawnSystem:addSpawnType(microbeTeeny, 1/6000, 40) spawnSystem:addSpawnType(microbePlankton, 1/32000, 40) @@ -367,20 +321,7 @@ local function setupEmitter() sceneNode.meshName = "molecule.mesh" entity:addComponent(sceneNode) -- Emitter test - local testEmitter = CompoundEmitterComponent() - entity:addComponent(testEmitter) - testEmitter.emissionRadius = 1.5 - testEmitter.maxInitialSpeed = 10 - testEmitter.minInitialSpeed = 2 - testEmitter.minEmissionAngle = Degree(0) - testEmitter.maxEmissionAngle = Degree(360) - testEmitter.particleLifeTime = 5000 - local timedEmitter = TimedCompoundEmitterComponent() - timedEmitter.compoundId = CompoundRegistry.getCompoundId("oxygen") - timedEmitter.particlesPerEmission = 1 - timedEmitter.potencyPerParticle = 3.0 - timedEmitter.emitInterval = 1000 - entity:addComponent(timedEmitter) + addEmitter2Entity(entity, "glucose") end function unlockToxin(entityId) @@ -393,17 +334,17 @@ function unlockToxin(entityId) return true end -function createStarterMicrobe(name, aiControlled) - local microbe = microbeSpawnFunctionGeneric(nil,"Default",aiControlled, name) - return microbe -end - local function setupPlayer() SpeciesRegistry.loadFromXML("../definitions/microbes.xml") - microbe = createStarterMicrobe(PLAYER_NAME, false) + microbe = microbeSpawnFunctionGeneric(nil, "Default", false, PLAYER_NAME) microbe.collisionHandler:addCollisionGroup("powerupable") Engine:playerData():lockedMap():addLock("Toxin") Engine:playerData():setActiveCreature(microbe.entity.id, GameState.MICROBE) + speciesEntity = Entity("defaultMicrobeSpecies") + species = SpeciesComponent("defaultMicrobeSpecies") + species:fromMicrobe(microbe) + speciesEntity:addComponent(species) + microbe.microbe.speciesName = "defaultMicrobeSpecies" end local function setupSound() @@ -462,6 +403,9 @@ local function createMicrobeStage(name) TimedLifeSystem(), CompoundMovementSystem(), CompoundAbsorberSystem(), + --PopulationSystem(), + PatchSystem(), + SpeciesSystem(), -- Physics RigidBodyInputSystem(), UpdatePhysicsSystem(), @@ -489,6 +433,7 @@ local function createMicrobeStage(name) setupBackground() setupCamera() setupEmitter() + setupSpecies() setupPlayer() setupSound() end, @@ -498,3 +443,4 @@ end GameState.MICROBE = createMicrobeStage("microbe") GameState.MICROBE_ALTERNATE = createMicrobeStage("microbe_alternate") +--Engine:setCurrentGameState(GameState.MICROBE) diff --git a/scripts/microbe_stage/species_system.lua b/scripts/microbe_stage/species_system.lua new file mode 100644 index 00000000000..53c7b08e84d --- /dev/null +++ b/scripts/microbe_stage/species_system.lua @@ -0,0 +1,186 @@ + +-------------------------------------------------------------------------------- +-- SpeciesComponent +-- +-- Holds information about an entity representing a species +-------------------------------------------------------------------------------- +class 'SpeciesComponent' (Component) + +SPECIES_NUM = 0 + +function SpeciesComponent:__init(name) + Component.__init(self) + self.num = SPECIES_NUM + SPECIES_NUM = SPECIES_NUM + 1 + if name == nil then + self.name = "noname"..self.num + else + self.name = name + end + + self.organelles = {} -- stores a table of organelle {q,r,name} tables + + self.avgCompoundAmounts = {} -- maps each compound name to the amount a new spawn should get. Nonentries are zero. + -- we could also add some measure of variability to make things more ...variable. + self.compoundPriorities = {} -- maps compound name to priority. +end + +--is this still todo? +--todo - store moar data +function SpeciesComponent:load(storage) + Component.load(self, storage) + self.name = storage:get("name", "") + self.compoundPriorities = {} + priorityData = storage:get("compoundPriorities", nil) + organelleData = storage:get("organelleData", nil) + self.organelles = {} + if organelleData ~= nil then + i = 1 + while organelleData:contains(""..i) do + organelle = {} + orgData = organelleData:get(""..i, nil) + if orgData ~= nil then + organelle.name = orgData:get("name", "") + organelle.q = orgData:get("q", 0) + organelle.r = orgData:get("r", 0) + end + self.organelles[i] = organelle + i = i + 1 + end + end +end + +--todo +function SpeciesComponent:storage() + local storage = Component.storage(self) + storage:set("name", self.name) + compoundPriorities = StorageContainer() + for k,v in pairs(self.compoundPriorities) do + compoundPriorities:set(k,v) + end + storage:set("compoundPriorities", compoundPriorities) + organelles = StorageContainer() + for i, org in ipairs(self.organelles) do + orgData = StorageContainer() + orgData:set("name", org.name) + orgData:set("q", org.q) + orgData:set("r", org.r) + organelles:set(""..i, orgData) + end + storage:set("organelleData", organelles) + return storage +end + +function SpeciesComponent:mutate(aiControlled, population) + --[[ + SpeciesComponent:mutate should call the correct evolution-handler + (Ie, trigger an entry into microbe editor for player; and auto-evo + for AI), and then create and add the new species to the system. + - uses the population to access information about + + For player mutation, this would be the entry point into the editor + + - note, this architecture may be reconsidered + + ]] +end + + +-- Given a newly-created microbe, this sets the organelles and all other species-specific microbe data +-- like agent codes, for example. +function SpeciesComponent:template(microbe) + microbe.microbe.speciesName = self.name + -- give it organelles + for i, orgdata in pairs(self.organelles) do + organelle = OrganelleFactory.makeOrganelle(orgdata) + microbe:addOrganelle(orgdata.q, orgdata.r, organelle) + end + + for compoundID, amount in pairs(self.avgCompoundAmounts) do + if amount ~= 0 then + microbe:storeCompound(compoundID, amount, false) + end + end + for compoundID, priority in pairs(self.compoundPriorities) do + if priority ~= 0 then + microbe:setDefaultCompoundPriority(compoundID, priority) + end + end + -- complimentary serving of atp + --newMicrobe:storeCompound(CompoundRegistry.getCompoundId("atp"), 10) + return microbe +end + +--[[ +Modify the species, using a microbe as the template for the new genome. + +]] +function SpeciesComponent:fromMicrobe(microbe) + microbe = microbe.microbe -- shouldn't break, I think + self.name = microbe.speciesName + --print("self.name: "..self.name) + -- Create species' organelle data + for i, organelle in pairs(microbe.organelles) do + --print(i) + local data = {} + data.name = organelle.name + data.q = organelle.position.q + data.r = organelle.position.r + self.organelles[i] = data + end +end + +REGISTER_COMPONENT("SpeciesComponent", SpeciesComponent) + +SPECIES_SIM_INTERVAL = 20000 + +-------------------------------------------------------------------------------- +-- SpeciesSystem +-- +-- System for estimating and simulating population count for various species +-------------------------------------------------------------------------------- +class 'SpeciesSystem' (System) + +function SpeciesSystem:__init() + System.__init(self) + + self.entities = EntityFilter( + { + SpeciesComponent + }, + true + ) + self.timeSinceLastCycle = 0 +end + +-- Override from System +function SpeciesSystem:init(gameState) + System.init(self, gameState) + self.entities:init(gameState) +end + +-- Override from System +function SpeciesSystem:shutdown() + self.entities:shutdown() + System.shutdown(self) +end + +-- Override from System +function SpeciesSystem:activate() + --[[ + This runs in two (three?) use-cases: + - First, it runs on game entry from main menu -- in this case, it must set up for new game + - Second, it runs on game entry from editor -- in this case, it can set up the new species + (in conjunction with stuff in editor, called on finish-click) + - Third (possibly) it might run on load. + --]] +end + +-- Override from System +function SpeciesSystem:update(_, milliseconds) + self.timeSinceLastCycle = self.timeSinceLastCycle + milliseconds + while self.timeSinceLastCycle > SPECIES_SIM_INTERVAL do + -- do mutation-management here + self.timeSinceLastCycle = self.timeSinceLastCycle - SPECIES_SIM_INTERVAL + end +end diff --git a/scripts/util.lua b/scripts/util.lua index f2e68dac28f..7ce65cf63ee 100644 --- a/scripts/util.lua +++ b/scripts/util.lua @@ -51,3 +51,102 @@ function sign(x) end end +--[[ +Decorate a function with once to ensure it only runs once. Ever. +]] +function once(fn) + local able = true + function foo(...) + local ret = nil + if able then + ret = fn(...) + able = false + end + if ret ~= nil then return ret end + end + return foo +end + +--[[ +We need an extension of once that provides each "once" with an accompanying "enable", that +simply allows the once to run again +]] + +function onceWithReset(fn) + local active = true + function run(...) + local ret = nil + if active then + ret = fn(...) + active = false + end + if ret ~= nil then return ret end + end + function enable() + active = true + end + return {run = run, enable = enable} +end + +--[[ +Decorates two functions at a time, forcing them to happen in alternating order +Example use-case: coordinating work done when moving between gamestates +]] +function forceAlternating(in_first, in_second) + local firsts_turn = true + function do_first(...) + local ret = nil + if firsts_turn then + if in_first ~= nil then ret = in_first(...) end + firsts_turn = false + end + return ret + end + function do_second(...) + local ret = nil + if firsts_turn == false then + if in_second ~= nil then ret = in_second(...) end + firsts_turn = true + end + return ret + end + return {first = do_first, second = do_second} +end + +--[[ +No idea if this works, but should make the decorated function run +only once per each instance of the object with the decorated method. +]] +function oncePer(fn) + function foo(...) + t = {...} + -- we need to know how the object is injected into the method -- by closure? by argument? + if t[1]["__oncePer__"] == nil then t[1]["__oncePer__"] = {} end + -- indexing the table with the function itself could be a source of issues + if t[1]["__oncePer__"][fn] == nil then + ret = fn(...) + t[1]["__oncePer__"][fn] = true + return ret + end + end + return foo +end + +--[[ +Memoizes the decorated function. +Could use some optimization, probably. +Should probably be done in C++, actually. + +It's actually quite a bit too ugly in lua, requiring loads of work to detect identity between arg tables +...without which it's useless, of course. +]] +function memoize(fn) + memo = {} + function foo(...) + if memo[{...}] == nil then + memo[{...}] = fn(...) + end + return memo[{...}] + end + return foo +end diff --git a/src/microbe_stage/species_registry.cpp b/src/microbe_stage/species_registry.cpp index a4a44bf5242..7885e0ecf59 100644 --- a/src/microbe_stage/species_registry.cpp +++ b/src/microbe_stage/species_registry.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "tinyxml.h" @@ -16,6 +17,7 @@ SpeciesRegistry::luaBindings() { .scope [ def("loadFromXML", &SpeciesRegistry::loadFromXML), + def("getSpeciesNames", &SpeciesRegistry::getSpeciesNames), def("getSize", &SpeciesRegistry::getSize), def("getCompoundPriority", &SpeciesRegistry::getCompoundPriority), def("getCompoundAmount", SpeciesRegistry::getCompoundAmount), @@ -52,7 +54,11 @@ speciesMap() { int SpeciesRegistry::getSize( const std::string& microbe_name) { - return speciesMap()[microbe_name].organelles.size(); + try { + return speciesMap()[microbe_name].organelles.size(); + } catch (...) { + return 0; + } } luabind::object @@ -60,10 +66,27 @@ SpeciesRegistry::getOrganelle( const std::string& microbe_name, int index) { luabind::object table = luabind::newtable(Game::instance().engine().luaState()); - Organelle organelle = speciesMap()[microbe_name].organelles[index]; - table["name"] = organelle.internalName; - table["q"] = organelle.q; - table["r"] = organelle.r; + try { + Organelle organelle = speciesMap()[microbe_name].organelles[index]; + table["name"] = organelle.internalName; + table["q"] = organelle.q; + table["r"] = organelle.r; + } catch (...) { + table["name"] = ""; + table["q"] = 0; + table["r"] = 0; + } + return table; +} + +luabind::object +SpeciesRegistry::getSpeciesNames() { + luabind::object table = luabind::newtable(Game::instance().engine().luaState()); + int i = 1; + for (std::string name : speciesMap() | boost::adaptors::map_keys) { + table[i] = name; + i++; + } return table; } @@ -71,14 +94,22 @@ double SpeciesRegistry::getCompoundPriority( const std::string& microbe_name, const std::string& compound_name) { - return speciesMap()[microbe_name].compounds[compound_name].priority; + try { + return speciesMap()[microbe_name].compounds[compound_name].priority; + } catch (...) { + return 0; + } } double SpeciesRegistry::getCompoundAmount( const std::string& microbe_name, const std::string& compound_name) { - return speciesMap()[microbe_name].compounds[compound_name].amount; + try { + return speciesMap()[microbe_name].compounds[compound_name].amount; + } catch (...) { + return 0; + } } void diff --git a/src/microbe_stage/species_registry.h b/src/microbe_stage/species_registry.h index 44a4a1f09c5..d0e0a5e7022 100644 --- a/src/microbe_stage/species_registry.h +++ b/src/microbe_stage/species_registry.h @@ -24,6 +24,7 @@ class SpeciesRegistry { * * Exposes: * - SpeciesRegistry::loadFromXML + * - SpeciesRegistry::getSpeciesNames * - SpeciesRegistry::getSize * - SpeciesRegistry::getOrganelle * - SpeciesRegistry::getCompoundPriority @@ -56,6 +57,9 @@ class SpeciesRegistry { int index ); + static luabind::object + getSpeciesNames(); + static double getCompoundPriority( const std::string& microbe_name,