FILES ----- player.config behaviors\npc\crewmember.behavior npcs\bmain.lua npcs\villager.npctype npcs\crew\crewmember.npctype npcs\crew\crewmemberchemist.npctype [NEW] npcs\crew\crewmemberchemistblue.npctype [NEW] npcs\crew\crewmemberchemistgreen.npctype [NEW] npcs\crew\crewmemberchemistorange.npctype [NEW] npcs\crew\crewmemberchemistyellow.npctype npcs\crew\crewmemberengineer.npctype [NEW] npcs\crew\crewmemberjanitor.npctype npcs\crew\crewmembermechanic.npctype npcs\crew\crewmembermedic.npctype [NEW] npcs\crew\crewmembertrendsetter.npctype scripts\util.lua scripts\companions\capturable.lua [NEW] scripts\companions\crewbenefits.lua scripts\companions\petspawner.lua scripts\companions\player.lua scripts\companions\recruitable.lua scripts\companions\recruitspawner.lua DIFFS ----- player.config 776c776 < "crewEffects" : { --- > "crewBenefits" : { behaviors\npc\crewmember.behavior 3d2 < "description": "", 12a12 > "/scripts/actions/position.lua", 19c19,20 < "teleportCooldown": 40 --- > "cantreachCooldown": 40, > "teleportRange": 50 498a500,620 > "title": "sequence", > "type": "composite", > "name": "sequence", > "parameters": { > "type": "sliding", > "state": "off", > "fromEntity": "attackTarget", > "toEntity": "fleeTarget" > }, > "children": [ > { > "title": "isFollowingRecruiter", > "type": "action", > "name": "isFollowingRecruiter", > "parameters": {} > }, > { > "title": "recruiterEntity", > "type": "action", > "name": "recruiterEntity", > "parameters": {}, > "output": { > "entity": "player" > } > }, > { > "title": "inverter", > "type": "decorator", > "name": "inverter", > "parameters": {}, > "child": { > "title": "entityInRange", > "type": "action", > "name": "entityInRange", > "parameters": { > "entity": "player", > "range": "", > "position": "self" > } > } > }, > { > "title": "entityPosition", > "type": "action", > "name": "entityPosition", > "parameters": { > "entity": "player" > }, > "output": { > "position": "playerPosition" > } > }, > { > "title": "groundPosition", > "type": "action", > "name": "groundPosition", > "parameters": { > "avoidLiquid": true, > "maxHeight": 5, > "minHeight": -5, > "position": "playerPosition" > }, > "output": { > "position": "playerPosition" > } > }, > { > "title": "addEphemeralEffect", > "type": "action", > "name": "addEphemeralEffect", > "parameters": { > "name": "invisible" > } > }, > { > "title": "addEphemeralEffect", > "type": "action", > "name": "addEphemeralEffect", > "parameters": { > "name": "blink" > } > }, > { > "title": "timer", > "type": "action", > "name": "timer", > "parameters": { > "time": 0.25 > }, > "output": { > "ratio": "" > } > }, > { > "title": "setPosition", > "type": "action", > "name": "setPosition", > "parameters": { > "footPosition": false, > "position": "playerPosition" > } > }, > { > "title": "timer", > "type": "action", > "name": "timer", > "parameters": { > "time": 0.25 > } > }, > { > "title": "removeEphemeralEffect", > "type": "action", > "name": "removeEphemeralEffect", > "parameters": { > "name": "invisible" > } > } > ] > }, > { 515a638,647 > "title": "entityInRange", > "type": "action", > "name": "entityInRange", > "parameters": { > "entity": "target", > "range": "", > "position": "self" > } > }, > { 895c1027 < "cooldown": "", --- > "cooldown": "", 906c1038 < "title": "addEphemeralEffect", --- > "title": "groundPosition", 908c1040 < "name": "addEphemeralEffect", --- > "name": "groundPosition", 910c1042,1048 < "name": "beamoutandteleport" --- > "avoidLiquid": true, > "maxHeight": 5, > "minHeight": -5, > "position": "playerPosition" > }, > "output": { > "position": "playerPosition" 914,968c1052,1067 < "title": "dynamic", < "type": "composite", < "name": "dynamic", < "parameters": {}, < "children": [ < { < "title": "sequence", < "type": "composite", < "name": "sequence", < "parameters": {}, < "children": [ < { < "title": "receivedNotification", < "type": "action", < "name": "receivedNotification", < "parameters": { < "type": "performTeleport" < }, < "output": { < "source": "", < "target": "" < } < }, < { < "title": "setPosition", < "type": "action", < "name": "setPosition", < "parameters": { < "footPosition": false, < "position": "playerPosition" < } < }, < { < "title": "timer", < "type": "action", < "name": "timer", < "parameters": { < "time": [ < 2, < 2 < ] < }, < "output": { < "ratio": "" < } < } < ] < }, < { < "title": "runner", < "type": "action", < "name": "runner", < "parameters": {} < } < ] --- > "title": "faceEntity", > "type": "action", > "name": "faceEntity", > "parameters": { > "entity": "player" > } > }, > { > "title": "sayToEntity", > "type": "action", > "name": "sayToEntity", > "parameters": { > "dialogType": "dialog.crewmember.cantreach", > "entity": "player", > "tags": {} > } npcs\bmain.lua 258a259,264 > function setNpcItemSlot(slotName, item) > npc.setItemSlot(slotName, item) > storage.itemSlots = storage.itemSlots or {} > storage.itemSlots[string.lower(slotName)] = item > end > 280,282c286 < npc.setItemSlot(args.slot, item) < storage.itemSlots = storage.itemSlots or {} < storage.itemSlots[string.lower(args.slot)] = item --- > setNpcItemSlot(args.slot, item) npcs\villager.npctype 18c18,21 < [1, "crewmemberbiologist"], --- > [0.25, "crewmemberchemistblue"], > [0.25, "crewmemberchemistgreen"], > [0.25, "crewmemberchemistyellow"], > [0.25, "crewmemberchemistorange"], 22c25,26 < [1, "crewmemberphysicist"] --- > [1, "crewmemberjanitor"], > [1, "crewmembertrendsetter"] npcs\crew\crewmember.npctype 87a88,94 > }, > "cantreach" : { > "default" : { > "default" : [ > "I can't reach you!" > ] > } npcs\crew\crewmemberchemist.npctype 4a5,7 > // This npctype is used just as a base for the other crewmemberchemist* > // npctypes. Don't use it directly for spawning NPCs. > 10,12c13 < "field" : "Pharmaceutical", < < "ephemeralEffectsFromItems" : true --- > "field" : "Pharmaceutical" 33,36d33 < "chest" : [ { "name" : "protectoratechest" } ], < "legs" : [ { "name" : "protectoratepants" } ], < "primary" : [ "bluestim", "greenstim", "orangestim", "yellowstim" ], < "sheathedprimary" : [ "npcassaultrifle", "npcbroadsword" ] npcs\crew\crewmemberengineer.npctype 10,15c10,19 < "field" : "Engineering" < }, < "roleEffects" : { < "multipliers" : { < "fuelEfficiency" : 1.1 < } --- > "field" : "Engineering", > > "benefits" : [ > { > "type" : "PeriodicMultiplier", > "property" : "ship.fuelEfficiency", > "value" : 1.1, > "period" : 1000 > } > ] 24a29,35 > ] > } > }, > "shipImprovementApplied" : { > "default" : { > "default" : [ > "It's done! Fuel efficiency is up another 10%! I'll keep working on it!" npcs\crew\crewmembermechanic.npctype 10,16c10,17 < "field" : "Technical" < }, < "roleEffects" : { < "periodicIncreases" : { < "ship.maxFuel" : { < "amount" : 100, < "delay" : 1000 // seconds --- > "field" : "Technical", > > "benefits" : [ > { > "type" : "PeriodicIncrease", > "property" : "ship.maxFuel", > "value" : 100, > "period" : 1000 18c19 < } --- > ] npcs\crew\crewmembermedic.npctype 10,13c10,17 < "field" : "Medical" < }, < "roleEffects" : { < "applyRegeneration" : true --- > "field" : "Medical", > > "benefits" : [ > { > "type" : "Regeneration", > "value" : 1 > } > ] scripts\util.lua 151a152,159 > function util.mapWithKeys(t, func, newTable) > newTable = newTable or {} > for k,v in pairs(t) do > newTable[k] = func(k,v) > end > return newTable > end > scripts\companions\capturable.lua 19c19 < return capturable.captureStatus() --- > return { status = capturable.captureStatus() } scripts\companions\petspawner.lua 21a22 > self.storage = json.storage 53a55 > storage = self.storage, 87a90 > 88a92 > scriptConfig.initialStorage = util.mergeTable(scriptConfig.initialStorage or {}, self.storage or {}) 232,233c236,238 < promises:add(world.sendEntityMessage(self.uniqueId, self.statusRequestMessage, persistentEffects), function (status) < self.status = status --- > promises:add(world.sendEntityMessage(self.uniqueId, self.statusRequestMessage, persistentEffects), function (state) > self.status = state.status > self.storage = state.storage scripts\companions\player.lua 42a43 > message.setHandler("recruits.setActiveCrewItemSlots", simpleHandler(setActiveCrewItemSlots)) 85,88d85 < < local fuelEfficiency = recruitSpawner:getShipMultiplier("fuelEfficiency", 1.0) < world.setProperty("ship.fuelEfficiency", fuelEfficiency) < 171a169,170 > logCrewSize() > 280,282c279 < -- See if we can add recruitUuid to our ship crew and followers lists without < -- breaching any limits < function checkCrewLimits(recruitUuid) --- > function logCrewSize() 284a282 > end 285a284,286 > -- See if we can add recruitUuid to our ship crew and followers lists without > -- breaching any limits > function checkCrewLimits(recruitUuid) 288a290 > logCrewSize() 293a296 > logCrewSize() 347a351,374 > end > > function setActiveCrewItemSlots(slots, recruitUuid) > local items = {} > local slotNames = {} > for slotName,playerSlots in pairs(slots) do > table.insert(slotNames, slotName) > for _,playerSlot in pairs(playerSlots) do > if player.equippedItem(playerSlot) then > items[slotName] = player.equippedItem(playerSlot) > break > end > end > end > > for _,recruit in pairs(recruitSpawner.recruits) do > recruit:setItemSlots(slotNames, items) > end > if recruitUuid then > local recruit = recruitSpawner:getRecruit(recruitUuid) > if recruit then > recruit:setItemSlots(slotNames, items) > end > end scripts\companions\recruitable.lua 20a21 > message.setHandler("recruit.setItemSlots", simpleHandler(recruitable.setItemSlots)) 129,150d129 < function recruitable.generateRoleEffects() < local ephemeralEffects = nil < < if config.getParameter("crew.role.ephemeralEffectsFromItems") then < for _,item in pairs(getHeldItems()) do < if root.itemType(item.name or item) == "consumable" then < local itemConfig = root.itemConfig(item) < local itemEffects = (itemConfig.config.effects or {})[1] or {} < < ephemeralEffects = ephemeralEffects or {} < util.appendLists(ephemeralEffects, itemEffects) < end < end < end < < storage.recruitRoleEffects = { < ephemeralEffects = ephemeralEffects < } < < return storage.recruitRoleEffects < end < 153d131 < local roleEffects = config.getParameter("crew.roleEffects") or storage.recruitRoleEffects or recruitable.generateRoleEffects() 159d136 < initialStorage = preservedStorage(), 161,162c138 < rank = rank, < roleEffects = roleEffects --- > rank = rank 183d158 < roleEffects = roleEffects, 184a160 > storage = preservedStorage(), 227c203,206 < return getCurrentStatus() --- > return { > status = getCurrentStatus(), > storage = preservedStorage() > } 245a225,232 > > local interactAction = config.getParameter("crew.interactAction") > if interactAction then > local data = config.getParameter("crew.interactData", {}) > data.messageArgs = data.messageArgs or {} > table.insert(data.messageArgs, recruitable.recruitUuid()) > return { interactAction, data } > end 282a270,277 > end > > function recruitable.setItemSlots(slotNames, items) > if not recruitable.ownerUuid() then return end > for _, slotName in pairs(slotNames) do > setNpcItemSlot(slotName, items[slotName]) > end > return preservedStorage() scripts\companions\recruitspawner.lua 1a2 > require "/scripts/companions/crewbenefits.lua" 17,19c18,19 < self.role = root.npcConfig(self.spawnConfig.type).scriptConfig.crew.role.name < self.roleEffects = json.roleEffects < self.shipTimers = json.shipTimers or {} --- > self.role = root.npcConfig(self.spawnConfig.type).scriptConfig.crew.role > self.benefits = loadBenefits(self.role.benefits, json.benefits) 34,35c34 < json.roleEffects = self.roleEffects < json.shipTimers = self.shipTimers --- > json.benefits = self.benefits:store() 39,69c38,40 < function Recruit:setupTimers(json) < -- Timers here are for things like levelling up, periodic improvements to < -- the ship, etc. < -- These are currently reset when the NPC is instructed to follow the player. < for varName, _ in pairs(self.roleEffects.periodicIncreases or {}) do < if not self.shipTimers[varName] then < self.shipTimers[varName] = world.time() < end < end < end < < function Recruit:shipUpdate(dt) < self:setupTimers(dt) < < local time = world.time() < local increases = self.roleEffects.periodicIncreases < < for varName, startTime in pairs(self.shipTimers) do < local elapsed = time - startTime < if increases and increases[varName] and elapsed >= increases[varName].delay then < world.logInfo("startTime = %s, now time = %s, elapsed = %s, delay = %s", startTime, time, elapsed, increases[varName].delay) < self.shipTimers[varName] = time < < world.setProperty(varName, world.getProperty(varName) + increases[varName].amount) < if self.uniqueId then < world.sendEntityMessage(self.uniqueId, "notify", { < type = "shipImprovementApplied", < sourceId = entity.id() < }) < end < end --- > function Recruit:sendMessage(...) > if self.uniqueId and not self.spawning then > return world.sendEntityMessage(self.uniqueId, ...) 77c48 < role = self.role or "Soldier", --- > role = self.role.name or "Soldier", 87a59,69 > function Recruit:setItemSlots(slotNames, items) > local promise = self:sendMessage("recruit.setItemSlots", slotNames, items) > if promise then > promises:add(promise, function (storage) > -- Receive an updated storage object -- which contains the npc's current > -- equipment -- so we can respawn them with these items too. > self.storage = storage > end) > end > end > 181a164,167 > function recruitSpawner:getRecruit(recruitUuid) > return self.recruits[recruitUuid] or self.shipCrew[recruitUuid] > end > 188,191c174,175 < util.appendLists(effects, (recruit.roleEffects or {}).persistentEffects or {}) < if recruit.roleEffects.applyRegeneration then < regeneration = regeneration + 1 < end --- > util.appendLists(effects, recruit.benefits:persistentEffects()) > regeneration = regeneration + recruit.benefits:regenerationAmount() 195c179 < local regenEffects = config.getParameter("crewEffects.regeneration") --- > local regenEffects = config.getParameter("crewBenefits.regeneration") 200,205d183 < -- Also get all the ephemeral effects we get upon leaving the ship < -- persistently until we leave the ship < util.appendLists(effects, util.map(self:getShipEphemeralEffects(), function (effect) < return effect.effect or effect < end)) < 213c191 < util.appendLists(effects, (recruit.roleEffects or {}).ephemeralEffects or {}) --- > util.appendLists(effects, recruit.benefits:ephemeralEffects()) 218,227d195 < function recruitSpawner:getShipMultiplier(multiplierName, baseValue) < local value = baseValue < self:forEachCrewMember(function (recruit) < if recruit.roleEffects.multipliers then < value = value * (recruit.roleEffects.multipliers[multiplierName] or 1.0) < end < end) < return value < end < 235,252d202 < function recruitSpawner:_respawnRecruits(recruits) < for uuid, recruit in pairs(recruits) do < if recruit:dead() or (recruit.persistent and not recruit.uniqueId) then < recruit:despawn() < recruit.status = nil < recruit:spawn(nil, { < scriptConfig = { < diedAndRespawned = true, < initialStorage = { < followingOwner = false, < behaviorFollowing = false < } < } < }) < end < end < end < 259,260d208 < self:_respawnRecruits(self.recruits) < self:_respawnRecruits(self.shipCrew) 263c211,225 < recruit:shipUpdate(dt) --- > if recruit:dead() or (recruit.persistent and not recruit.uniqueId) then > recruit:despawn() > recruit.status = nil > recruit:spawn(nil, { > scriptConfig = { > diedAndRespawned = true, > initialStorage = { > followingOwner = false, > behaviorFollowing = false > } > } > }) > else > recruit.benefits:shipUpdate(recruit, dt) > end