ADDED dev.ct Index: dev.ct ================================================================== --- dev.ct +++ dev.ct @@ -0,0 +1,19 @@ +# starsoul development +this file contains information meant for those who wish to develop for Starsoul or build the game from trunk. do NOT add any story information, particularly spoilers; those go in src/lore.ct. + +## tooling +starsoul uses the following software in the development process: +* [*csound] to generate sound effects +* [*GNU make] to automate build tasks +* [*lua] to automate configure tasks + +## building +to run a trunk version of Starsoul, you'll need to install the above tools and run `make` from the base directory. this will: +* run lua scripts to generate necessary makefiles +* generate the game sound effects and install them in mods/starsoul/sounds + +## policy +* copyright of all submitted code must be reassigned to the maintainer. +* all code is to be indented with tabs and aligned with spaces; formatting is otherwise up to whoever is responsible for maintaining that code +* use [`camelCase], not [`snake_case] and CERTAINLY not [`SCREAMING_SNAKE_CASE] +* sounds effects should be contributed in the form of csound files; avoid adding audio files to the repository except for foley effects ADDED game.conf Index: game.conf ================================================================== --- game.conf +++ game.conf @@ -0,0 +1,5 @@ +title = Starsoul +author = velartrill +description = High-tech survival on a hostile alien world +allowed_mapgens = v7 +disabled_settings = !enable_damage, creative_mode ADDED mods/starsoul-building/init.lua Index: mods/starsoul-building/init.lua ================================================================== --- mods/starsoul-building/init.lua +++ mods/starsoul-building/init.lua @@ -0,0 +1,135 @@ +local lib = starsoul.mod.lib +local B = {} +starsoul.mod.building = B + +B.path = {} +-- this maps stage IDs to tables of the following form +--[[ { + part = { + ['starsoul_building:pipe'] = 'myMod:stage3'; + }; + tool = { + ['starsoul:scredriver'] = 'myMod:otherThing_stage1'; + ['starsoul:saw'] = function(node, tool) + minetest.replace_node(node, {name='myMod:stage1'}) + minetest.drop_item(node, 'starsoul_building:pipe') + end; + ['myMod:laserWrench'] = { + allow = function(node, tool) ... end; + handle = function(node, tool) ... end; + }; + }; +} ]] +-- it should only be written by special accessor functions! + +B.stage = lib.registry.mk 'starsoul_building:stage' +-- a stage consists of a list of pieces and maps from possible materials +-- / tool usages to succeeding stages in the build tree. note that all +-- used pieces must be defined before a stage is defined currently, due +-- to a lack of cross-registry dependency mechanisms. i will hopefully +-- improve vtlib to handle this condition eventually. +--[[ + starsoul.mod.building.stage.link(id, { + pieces = { + 'starsoul_building:foundation'; + 'starsoul_building:insulation'; -- offset ofsFac + {'starsoul_building:pipe', vector.new(-.5, 0, 0), 0};-- + {'starsoul_building:pipe', vector.new(-.5, 0, 0), 1}; + 'starsoul_building:insulation'; + 'starsoul_building:panel'; + }; + }) +]] + +B.piece = lib.registry.mk 'starsoul_building:piece' +-- a piece is used to produce stage definitions, by means of appending +-- nodeboxes with appropriate offsets. it also lists the recoverable +-- materials which can be obtained by destroying a stage containing +-- this piece using nano. part IDs should correspond with piece IDs +-- where possible +--[[ + starsoul.mod.building.piece.link(id, { + tex = 'myMod_part.png'; + height = 0.1; -- used for auto-offset + fab = { + element = {iron=10}; + }; + shape = { + type = "fixed"; + fixed = { ... }; + }; + }) +]] + +B.part = lib.registry.mk 'starsoul_building:part' +-- a part is implemented as a special craftitem with the proper callbacks +-- to index the registries and place/replace noes by reference to the +-- build tree. +--[[ + starsoul.mod.building.part.link(id, { + name = ''; -- display name + desc = ''; -- display desc + img = ''; -- display image + }) +]] + +B.stage.foreach('starsoul:stageGen', {}, function(id, e) + local box = {type = 'fixed', fixed = {}} + local tex = {} + local ofs = vector.new(0,0,0) + for idx, p in ipairs(e.pieces) do + local ho, pieceID, pos + if type(p) == 'string' then + pieceID, pos, ho = p, vector.zero(), 1.0 + else + pieceID, pos, ho = pc[1],pc[2],pc[3] + end + local pc = B.piece.db[pieceID] + pos = pos + ofs + if ho ~= 0.0 then + ofs = vector.offset(ofs, 0, pc.height) + end + local sh = lib.node.boxwarped(pc.shape, function(b) + -- { -x, -y, -z; + -- +x, +y, +z } + b[1] = b[1] + ofs.x b[4] = b[4] + ofs.x + b[2] = b[2] + ofs.y b[5] = b[5] + ofs.y + b[3] = b[3] + ofs.z b[6] = b[6] + ofs.z + end) + table.insert(box, sh) + if type(pc.tex) == 'string' then + table.insert(tex, pc.tex) + else + for i,t in ipairs(pc.tex) do + table.insert(tex, t or '') + end + end + end + minetest.register_node(id, { + description = 'Construction'; + drawtype = 'nodebox'; + paramtype = 'light'; + paramtype2 = e.stateful or 'none'; + textures = tex; + node_box = box; + group = { stage = 1 }; + _starsoul = { + stage = id; + }; + }) +end) + +function B.pathLink(from, kind, what, to) + if not B.path[from] then + B.path[from] = {part={}, tool={}} + end + local k = B.path[from][kind] + assert(k[what] == nil) + k[what] = to +end + +function B.pathFind(from, kind, what) + if not B.path[from] then return nil end + return B.path[from][kind][what] +end + ADDED mods/starsoul-building/mod.conf Index: mods/starsoul-building/mod.conf ================================================================== --- mods/starsoul-building/mod.conf +++ mods/starsoul-building/mod.conf @@ -0,0 +1,3 @@ +name = starsoul_building +depends = starsoul_electronics, starsoul +description = implements construction elements ADDED mods/starsoul-electronics/init.lua Index: mods/starsoul-electronics/init.lua ================================================================== --- mods/starsoul-electronics/init.lua +++ mods/starsoul-electronics/init.lua @@ -0,0 +1,904 @@ +local lib = starsoul.mod.lib + +local E = {} +starsoul.mod.electronics = E + +--------------------- +-- item registries -- +--------------------- + +-- a dynamo is any item that produces power and can be slotted into a power +-- source slot. this includes batteries, but also things like radiothermal +-- dynamos. +starsoul.item.dynamo = lib.registry.mk 'starsoul_electronics:dynamo' + +-- batteries hold a charge of power (measured in kJ). how much they can hold +-- (and how much power they can discharge?) depends on their quality +starsoul.item.battery = lib.registry.mk 'starsoul_electronics:battery' + +-- a battery has the properties: +-- class +-- |- capacity (J ): amount of energy the battery can hold +-- |- dischargeRate (W ): rate at which battery can supply power/be charged +-- |- decay (J/J): rate at which the battery capacity degrades while +-- discharging. decay=0 batteries require no maintenance; +-- decay=1 batteries are effectively disposable +-- |- leak (fac): charging inefficiency. depends on the energy storage +-- technology. when N J are drawn from a power source, +-- only (N*leak) J actually make it into the battery. +-- leak=0 is a supercapacitor, leak=1 is /dev/null +-- |- size (m): each suit has a limit to how big of a battery it can take +-- instance +-- |- degrade (mJ): how much the battery has degraded. instance max charge is +-- | determined by $capacity - @degrade +-- |- %wear ÷ 2¹⁶ : used as a factor to determine battery charge + +-- chips are standardized data storage hardware that can contain a certain amount +-- of software. in addition to their flash storage, they also provide a given amount +-- of working memory and processor power. processor power speeds up operations like +-- crafting, while programs require a certain amount of memory. +-- chips have a variable number of program slots and a single bootloader slot +-- +starsoul.item.chip = lib.registry.mk 'starsoul_electronics:chip' + +-- software is of one of the following types: +-- schematic: program for your matter compiler that enables crafting a given item. +-- output: the result +-- driver: inserted into a Core to control attached hardware +-- suitPower: provides suit functionality like nanoshredding or healing +-- passive powers are iterated on suit application/configuration and upon fst-tick +-- cost: what the software needs to run. some fields are fab-specific +-- energy: for fab, total energy cost of process in joules +-- for suitPassive, added suit power consumption in watts +starsoul.item.sw = lib.registry.mk 'starsoul_electronics:sw' +-- chip = lib.color(0, 0, .3); + +E.schematicGroups = lib.registry.mk 'starsoul_electronics:schematicGroups' +E.schematicGroupMembers = {} +E.schematicGroups.foreach('starsoul_electronics:ensure-memlist', {}, function(id,g) + E.schematicGroupMembers[id] = {} +end) +function E.schematicGroupLink(group, item) + table.insert(E.schematicGroupMembers[group], item) +end + +E.schematicGroups.link('starsoul_electronics:chip', { + title = 'Chip', icon = 'starsoul-item-chip.png'; + description = 'Standardized data storage and compute modules'; +}) + +E.schematicGroups.link('starsoul_electronics:battery', { + title = 'Battery', icon = 'starsoul-item-battery.png'; + description = 'Portable power storage cells are essential to all aspects of survival'; +}) + +E.schematicGroups.link('starsoul_electronics:decayCell', { + title = 'Decay Cell', icon = 'starsoul-item-decaycell.png'; + description = "Radioisotope generators can pack much more power into a smaller amount of space than conventional battery, but they can't be recharged, dump power and heat whether they're in use or not, and their power yield drop towards zero over their usable lifetime."; +}) + + +------------------------- +-- batteries & dynamos -- +------------------------- + +E.battery = {} +local function accessor(ty, fn) + return function(stack, ...) + local function fail() + error(string.format('object %q is not a %s', stack:get_name(), ty)) + end + + if not stack or stack:is_empty() then fail() end + + if minetest.get_item_group(stack:get_name(), ty) == 0 then fail() end + + return fn(stack, + stack:get_definition()._starsoul[ty], + stack:get_meta(), ...) + end +end + +-- return a wear level that won't destroy the item +local function safeWear(fac) return math.min(math.max(fac,0),1) * 0xFFFE end +local function safeWearToFac(w) return w/0xFFFE end + +-- E.battery.capacity(bat) --> charge (J) +E.battery.capacity = accessor('battery', function(stack, batClass, meta) + local dmg = meta:get_int('starsoul_electronics:battery_degrade') -- µJ/μW + local dmg_J = dmg / 1000 + return (batClass.capacity - dmg_J) +end) + +-- E.battery.charge(bat) --> charge (J) +E.battery.charge = accessor('battery', function(stack, batClass, meta) + local fac = 1 - safeWearToFac(stack:get_wear()) + return E.battery.capacity(stack) * fac +end) + +-- E.battery.dischargeRate(bat) --> dischargeRate (W) +E.battery.dischargeRate = accessor('battery', function(stack, batClass, meta) + local dmg = meta:get_int('starsoul_electronics:battery_degrade') -- µJ/μW + local dmg_W = dmg / 1000 + return batClass.dischargeRate - dmg_W +end); + + +-- E.battery.drawCurrent(bat, power, time, test) --> supply (J), wasteHeat (J) +-- bat = battery stack +-- power J = joules of energy user wishes to consume +-- time s = the amount of time available for this transaction +-- supply J = how much power was actually provided in $time seconds +-- wasteHeat J = how heat is generated in the process +-- test = if true, the battery is not actually modified +E.battery.drawCurrent = accessor('battery', function(s, bc, m, power, time, test) + local ch = E.battery.charge(s) + local maxPower = math.min(E.battery.dischargeRate(s)*time, power, ch) + ch = ch - maxPower + + if not test then + local degrade = m:get_int 'starsoul_electronics:battery_degrade' or 0 + degrade = degrade + maxPower * bc.decay + -- for each joule of power drawn, capacity degrades by `decay` J + -- this should ordinarily be on the order of mJ or smaller + m:set_int('starsoul_electronics:battery_degrade', degrade) + s:set_wear(safeWear(1 - (ch / E.battery.capacity(s)))) + end + + return maxPower, 0 -- FIXME specify waste heat +end) + +-- E.battery.recharge(bat, power, time) --> draw (J) +-- bat = battery stack +-- power J = joules of energy user wishes to charge the battery with +-- time s = the amount of time available for this transaction +-- draw J = how much power was actually drawn in $time seconds +E.battery.recharge = accessor('battery', function(s, bc, m, power, time) + local ch = E.battery.charge(s) + local cap = E.battery.capacity(s) + local maxPower = math.min(E.battery.dischargeRate(s)*time, power) + local total = math.min(ch + maxPower, cap) + s:set_wear(safeWear(1 - (total/cap))) + return maxPower, 0 -- FIXME +end) + +E.battery.setCharge = accessor('battery', function(s, bc, m, newPower) + local cap = E.battery.capacity(s) + local power = math.min(cap, newPower) + s:set_wear(safeWear(1 - (power/cap))) +end) + +E.dynamo = { kind = {} } + +E.dynamo.drawCurrent = accessor('dynamo', function(s,c,m, power, time, test) + return c.vtable.drawCurrent(s, power, time, test) +end) +E.dynamo.totalPower = accessor('dynamo', function(s,c,m) return c.vtable.totalPower(s) end) +E.dynamo.dischargeRate = accessor('dynamo', function(s,c,m) return c.vtable.dischargeRate (s) end) +E.dynamo.initialPower = accessor('dynamo', function(s,c,m) return c.vtable.initialPower(s) end) +E.dynamo.wasteHeat = accessor('dynamo', function(s,c,m) return c.vtable.wasteHeat(s) end) +-- baseline waste heat, produced whether or not power is being drawn. for batteries this is 0, but for +-- radiothermal generators it may be high + +E.dynamo.kind.battery = { + drawCurrent = E.battery.drawCurrent; + totalPower = E.battery.charge; + initialPower = E.battery.capacity; + dischargeRate = E.battery.dischargeRate; + wasteHeat = function() return 0 end; +}; + +starsoul.item.battery.foreach('starsoul_electronics:battery-gen', {}, function(id, def) + minetest.register_tool(id, { + short_description = def.name; + groups = { battery = 1; dynamo = 1; electronic = 1; }; + inventory_image = def.img or 'starsoul-item-battery.png'; + description = starsoul.ui.tooltip { + title = def.name; + desc = def.desc; + color = lib.color(0,.2,1); + props = { + { title = 'Optimal Capacity', affinity = 'info'; + desc = lib.math.si('J', def.capacity) }; + { title = 'Discharge Rate', affinity = 'info'; + desc = lib.math.si('W', def.dischargeRate) }; + { title = 'Charge Efficiency', affinity = 'info'; + desc = string.format('%s%%', (1-def.leak) * 100) }; + { title = 'Size', affinity = 'info'; + desc = lib.math.si('m', def.fab.size.print) }; + }; + }; + _starsoul = { + event = { + create = function(st, how) + if not how.gift then -- cheap hack to make starting batteries fully charged + E.battery.setCharge(st, 0) + end + end; + }; + fab = def.fab; + dynamo = { + vtable = E.dynamo.kind.battery; + }; + battery = def; + }; + }) +end) + + +-- to use the power functions, consider the following situation. you have +-- a high-tier battery charger that can draw 100kW. (for simplicity, assume +-- it supports only one battery). if you install a low-tier battery, and +-- the charging callback is called every five seconds, you might use +-- a `recharge` call that looks like +-- +-- starsoul.mod.electronics.battery.recharge(bat, 5 * 100*1e4, 5) +-- +-- this would offer the battery 500kJ over five seconds. the battery will +-- determine how much power it can actually make use of in 5 five seconds, +-- and then return that amount. +-- +-- always remember to save the battery back to its inventory slot after +-- modifying its ItemStack with one of these functions! + + +-- battery types +-- supercapacitor: low capacity, no degrade, high dischargeRate, no leak +-- chemical: high capacity, high degrade, mid dischargeRate, low leak + +-- battery tiers +-- makeshift: cheap, weak, low quality +-- imperial ("da red wunz go fasta"): powerful, low quality +-- commune ("snooty sophisticates"): limited power, high quality, expensive +-- usukwinya ("value engineering"): high power, mid quality, affordable +-- eluthrai ("uncompromising"): high power, high quality, wildly expensive +-- firstborn ("god-tier"): exceptional + +local batteryTiers = { + makeshift = { + name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1, + fab = starsoul.type.fab { + metal = {copper=10}; + }; + desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods."; + complexity = 1; + sw = {rarity = 1}; + }; + imperial = { + name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; + fab = starsoul.type.fab { + metal = {copper=15, iron = 20}; + size = { print = 0.1 }; + }; + desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs."; + drm = 1; + complexity = 2; + sw = {rarity = 2}; + }; + commune = { + name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; + fab = starsoul.type.fab { + metal = {vanadium=50, steel=10}; + size = { print = 0.05 }; + }; + desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry."; + complexity = 5; + sw = {rarity = 3}; + }; + usukwinya = { + name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5, + fab = starsoul.type.fab { + metal = {vanadium=30, argon=10}; + size = { print = 0.07 }; + }; + desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost."; + drm = 2; + sw = {rarity = 10}; + complexity = 15; + }; + eluthrai = { + name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5, + fab = starsoul.type.fab { + metal = {beryllium=20, platinum=20, technetium = 1, cinderstone = 10 }; + size = { print = 0.03 }; + }; + desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves."; + complexity = 200; + sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space?? + }; + firstborn = { + name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3; + fab = starsoul.type.fab { + metal = {neodymium=20, xenon=150, technetium=5, sunsteel = 10 }; + crystal = {astrite = 1}; + size = { print = 0.05 }; + }; + desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up."; + complexity = 1000; + sw = {rarity = 0}; -- lol no + }; +} + +local batterySizes = { + small = {name = 'Small', capacity = .5, dischargeRate = .5, complexity = 1, matMult = .5, fab = starsoul.type.fab {size={print=0.1}}}; + mid = { capacity = 1, dischargeRate = 1, complexity = 1, matMult = 1, fab = starsoul.type.fab {size={print=0.3}}}; + large = {name = 'Large', capacity = 2, dischargeRate = 1.5, complexity = 1, matMult = 1.5, fab = starsoul.type.fab {size={print=0.5}}}; + huge = {name = 'Huge', capacity = 3, dischargeRate = 2, complexity = 1, matMult = 2, fab = starsoul.type.fab {size={print=0.8}}}; +} + +local batteryTypes = { + supercapacitor = { + name = 'Supercapacitor'; + desc = 'Room-temperature superconductors make for very reliable, high-dischargeRate, but low-capacity batteries.'; + fab = starsoul.type.fab { + metal = { enodium = 5 }; + size = {print=0.8}; + }; + sw = { + cost = { + cycles = 5e9; -- 5 bil cycles + ram = 10e9; -- 10GB + }; + pgmSize = 2e9; -- 2GB + rarity = 5; + }; + capacity = 50e3, dischargeRate = 1000; + leak = 0, decay = 1e-6; + + complexity = 3; + }; + chemical = { + name = 'Chemical'; + desc = ''; + fab = starsoul.type.fab { + element = { lithium = 3}; + metal = {iron = 5}; + size = {print=1.0}; + }; + sw = { + cost = { + cycles = 1e9; -- 1 bil cycles + ram = 2e9; -- 2GB + }; + pgmSize = 512e6; -- 512MB + rarity = 2; + }; + capacity = 200e3, dischargeRate = 200; + leak = 0.2, decay = 1e-2; + complexity = 1; + }; + carbon = { + name = 'Carbon'; + desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.'; + capacity = 1; + fab = starsoul.type.fab { + element = { carbon = 40 }; + size = {print=0.5}; + }; + sw = { + cost = { + cycles = 50e9; -- 50 bil cycles + ram = 64e9; -- 64GB + }; + pgmSize = 1e9; -- 1GB + rarity = 10; + }; + capacity = 100e3, dischargeRate = 500; + leak = 0.1, decay = 1e-3; + complexity = 10; + }; + hybrid = { + name = 'Hybrid'; + desc = ''; + capacity = 1; + fab = starsoul.type.fab { + element = { + lithium = 3; + }; + metal = { + iron = 5; + }; + size = {print=1.5}; + }; + sw = { + cost = { + cycles = 65e9; -- 65 bil cycles + ram = 96e9; -- 96GB + }; + pgmSize = 5e9; -- 5GB + rarity = 15; + }; + capacity = 300e3, dischargeRate = 350; + leak = 0.3, decay = 1e-5; + complexity = 30; + }; +} + +local function elemath(dest, src, mult) + dest = dest or {} + for k,v in pairs(src) do + if not dest[k] then dest[k] = 0 end + dest[k] = dest[k] + v*mult + end + return dest +end + +for bTypeName, bType in pairs(batteryTypes) do +for bTierName, bTier in pairs(batteryTiers) do +for bSizeName, bSize in pairs(batterySizes) do + -- elemath(elementCost, bType.fab.element or {}, bSize.matMult) + -- elemath(elementCost, bTier.fab.element or {}, bSize.matMult) + -- elemath(metalCost, bType.fab.metal or {}, bSize.matMult) + -- elemath(metalCost, bTier.fab.metal or {}, bSize.matMult) + local fab = bType.fab + bTier.fab + bSize.fab + starsoul.type.fab { + element = {copper = 10, silicon = 5}; + } + local baseID = string.format('battery_%s_%s_%s', + bTypeName, bTierName, bSizeName) + local id = 'starsoul_electronics:'..baseID + local name = string.format('%s %s Battery', bTier.name, bType.name) + if bSize.name then name = bSize.name .. ' ' .. name end + local function batStat(s) + if s == 'size' then + --return bType.fab[s] * (bTier.fab[s] or 1) * (bSize.fab[s] or 1) + return fab.size and fab.size.print or 1 + else + return bType[s] * (bTier[s] or 1) * (bSize[s] or 1) + end + end + + local swID = 'starsoul_electronics:schematic_'..baseID + fab.reverseEngineer = { + complexity = bTier.complexity * bSize.complexity * bType.complexity; + sw = swID; + } + fab.flag = {print=true} + + starsoul.item.battery.link(id, { + name = name; + desc = table.concat({ + bType.desc or ''; + bTier.desc or ''; + bSize.desc or ''; + }, ' '); + + fab = fab; + + capacity = batStat 'capacity'; + dischargeRate = batStat 'dischargeRate'; + leak = batStat 'leak'; + decay = batStat 'decay'; + }) + + local rare + if bType.sw.rarity == 0 or bTier.sw.rarity == 0 then + -- rarity is measured such that the player has a 1/r + -- chance of finding a given item, or if r=0, no chance + -- whatsoever (the sw must be obtained e.g. by reverse- + -- engineering alien tech) + rare = 0 + else + rare = bType.sw.rarity + bTier.sw.rarity + end + + starsoul.item.sw.link(swID, { + kind = 'schematic'; + name = name .. ' Schematic'; + output = id; + size = bType.sw.pgmSize; + cost = bType.sw.cost; + rarity = rare; + }) + + E.schematicGroupLink('starsoul_electronics:battery', swID) + +end end end + + +----------- +-- chips -- +----------- + +E.sw = {} +function E.sw.findSchematicFor(item) + local id = ItemStack(item):get_name() + print(id) + local fm = minetest.registered_items[id]._starsoul + if not (fm and fm.fab and fm.fab.reverseEngineer) then return nil end + local id = fm.fab.reverseEngineer.sw + return id, starsoul.item.sw.db[id] +end + +E.chip = { file = {} } +do local T,G = lib.marshal.t, lib.marshal.g + -- love too reinvent unions from first principles + E.chip.data = G.struct { + label = T.str; + uuid = T.u64; + files = G.array(16, G.class(G.struct { + kind = G.enum { + 'sw'; -- a piece of installed software + 'note'; -- a user-readable text file + 'research'; -- saved RE progress + 'genome'; -- for use with plant biosequencer? + 'blob'; -- opaque binary blob, so 3d-pty mods can use the + -- file mechanism to store arbirary data. + }; + drm = T.u8; -- inhibit copying + name = T.str; + body = T.text; + }, function(file) -- enc + local b = E.chip.file[file.kind].enc(file.body) + return { + kind = file.kind; + drm = file.drm; + name = file.name; + body = b; + } + end, function(file) -- dec + local f, ns = E.chip.file[file.kind].dec(file.body) + file.body = f + return file, ns + end)); + bootSlot = T.u8; -- indexes into files; 0 = no bootloader + } + E.chip.file.sw = G.struct { + pgmId = T.str; + conf = G.array(16, G.struct { + key = T.str, value = T.str; + }); + } + E.chip.file.note = G.struct { + author = T.str; + entries = G.array(16, G.struct { + title = T.str; + body = T.str; + }); + } + E.chip.file.research = G.struct { + itemId = T.str; + progress = T.clamp; + } + E.chip.file.blob = G.struct { + kind = T.str; -- MT ID that identifies a blob file type belonging to an external mod + size = T.u8; -- this must be manually reported since we don't know how to evaluate it + data = T.text; + } + function E.chip.fileSize(file) + -- boy howdy + if file.kind == 'blob' then + return file.body.size + elseif file.kind == 'note' then + local sz = 0x10 + #file.body.author + for _, e in pairs(file.body.entries) do + sz = sz + #e.title + #e.body + 0x10 -- header overhead + end + return sz + elseif file.kind == 'research' then + local re = assert(minetest.registered_items[file.body.itemId]._starsoul.fab.reverseEngineer) + return starsoul.item.sw.db[re.sw].size * file.body.progress + elseif file.kind == 'sw' then + return starsoul.item.sw.db[file.body.pgmId].size + elseif file.kind == 'genome' then + return 0 -- TODO + end + end + local metaKey = 'starsoul_electronics:chip' + function E.chip.read(chip) + local m = chip:get_meta() + local blob = m:get_string(metaKey) + if blob and blob ~= '' then + return E.chip.data.dec(lib.str.meta_dearmor(blob)) + else -- prepare to format the chip + return { + label = ''; + bootSlot = 0; + uuid = math.floor(math.random(0,2^32)); + files = {}; + } + end + end + function E.chip.write(chip, data) + local m = chip:get_meta() + m:set_string(metaKey, lib.str.meta_armor(E.chip.data.enc(data))) + E.chip.update(chip) + end + function E.chip.fileOpen(chip, inode, fn) + local c = E.chip.read(chip) + if fn(c.files[inode]) then + E.chip.write(chip, c) + return true + end + return false + end + function E.chip.fileWrite(chip, inode, file) + local c = E.chip.read(chip) + c.files[inode] = file + E.chip.write(chip, c) + end + function E.chip.usedSpace(chip, d) + d = d or E.chip.read(chip) + local sz = 0 + for _, f in pairs(d.files) do + sz = sz + E.chip.fileSize(f) + end + return sz + end + function E.chip.freeSpace(chip, d) + local used = E.chip.usedSpace(chip,d) + local max = assert(chip:get_definition()._starsoul.chip.flash) + return max - used + end + function E.chip.install(chip, file) + -- remember to write out the itemstack after using this function! + local d = E.chip.read(chip) + if E.chip.freeSpace(chip, d) - E.chip.fileSize(file) >= 0 then + table.insert(d.files, file) + E.chip.write(chip, d) + return true + else + return false + end + end +end + +function E.chip.files(ch) + local m = ch:get_meta() + if not m:contains 'starsoul_electronics:chip' then + return nil + end + local data = E.chip.read(ch) + local f = 0 + return function() + f = f + 1 + return data.files[f], f + end +end + +function E.chip.describe(ch, defOnly) + local def, data if defOnly then + def, data = ch, {} + else + def = ch:get_definition() + local m = ch:get_meta() + if m:contains 'starsoul_electronics:chip' then + data = E.chip.read(ch) + else + data = {} + defOnly = true + end + def = assert(def._starsoul.chip) + end + local props = { + {title = 'Clock Rate', affinity = 'info'; + desc = lib.math.si('Hz', def.clockRate)}; + {title = 'RAM', affinity = 'info'; + desc = lib.math.si('B', def.ram)}; + } + if not defOnly then + table.insert(props, { + title = 'Free Storage', affinity = 'info'; + desc = lib.math.si('B', E.chip.freeSpace(ch, data)) .. ' / ' + .. lib.math.si('B', def.flash); + }) + local swAffMap = { + schematic = 'schematic'; + suitPower = 'ability'; + driver = 'driver'; + } + for i, e in ipairs(data.files) do + local aff = 'neutral' + local name = e.name + local disabled = false + if e.kind == 'sw' then + for _,cf in pairs(e.body.conf) do + if cf.key == 'disable' and cf.value == 'yes' then + disabled = true + break + end + end + local sw = starsoul.item.sw.db[e.body.pgmId] + aff = swAffMap[sw.kind] or 'good' + if name == '' then name = sw.name end + end + name = name or '' + table.insert(props, disabled and { + title = name; + affinity = aff; + desc = ''; + } or { + --title = name; + affinity = aff; + desc = name; + }) + end + else + table.insert(props, { + title = 'Flash Storage', affinity = 'info'; + desc = lib.math.si('B', def.flash); + }) + end + return starsoul.ui.tooltip { + title = data.label and data.label~='' and string.format('<%s>', data.label) or def.name; + color = lib.color(.6,.6,.6); + desc = def.desc; + props = props; + }; +end + +function E.chip.update(chip) + chip:get_meta():set_string('description', E.chip.describe(chip)) +end + +starsoul.item.chip.foreach('starsoul_electronics:chip-gen', {}, function(id, def) + minetest.register_craftitem(id, { + short_description = def.name; + description = E.chip.describe(def, true); + inventory_image = def.img or 'starsoul-item-chip.png'; + groups = {chip = 1}; + _starsoul = { + fab = def.fab; + chip = def; + }; + }) +end) + +-- in case other mods want to define their own tiers +E.chip.tiers = lib.registry.mk 'starsoul_electronics:chipTiers' +E.chip.tiers.meld { + -- GP chips + tiny = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e9, size = 1}; + small = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e8, size = 3}; + med = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e7, size = 6}; + large = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e6, size = 8}; + -- specialized chips + compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e8, size = 4}; + data = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e5, size = 4}; + lp = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e10, size = 4}; + carbon = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e9, size = 2, circ='carbon'}; +} + +E.chip.tiers.foreach('starsoul_electronics:genChips', {}, function(id, t) + id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id) + local circMat = t.circ or 'silicon'; + starsoul.item.chip.link(id, { + name = t.name; + clockRate = t.clockRate; + flash = t.flash; + ram = t.ram; + powerEfficiency = t.powerEfficiency; -- cycles per joule + fab = { + flag = { + silicompile = true; + }; + time = { + silicompile = t.size * 24*60; + }; + cost = { + energy = 50e3 + t.size * 15e2; + }; + element = { + [circMat] = 50 * t.size; + copper = 30; + gold = 15; + }; + }; + }) +end) + +function E.chip.findBest(test, ...) + local chip, bestFitness + for id, c in pairs(starsoul.item.chip.db) do + local fit, fitness = test(c, ...) + if fit and (bestFitness == nil or fitness > bestFitness) then + chip, bestFitness = id, fitness + end + end + return chip, starsoul.item.chip.db[chip], bestFitness +end + +function E.chip.findForStorage(sz) + return E.chip.findBest(function(c) + return c.flash >= sz, -math.abs(c.flash - sz) + end) +end + +function E.chip.sumCompute(chips) + local c = { + cycles = 0; + ram = 0; + flashFree = 0; + powerEfficiency = 0; + } + local n = 0 + for _, e in pairs(chips) do + n = n + 1 + if not e:is_empty() then + local ch = e:get_definition()._starsoul.chip + c.cycles = c.cycles + ch.clockRate + c.ram = c.ram + ch.clockRate + c.flashFree = c.flashFree + E.chip.freeSpace(e) + c.powerEfficiency = c.powerEfficiency + ch.powerEfficiency + end + end + if n > 0 then c.powerEfficiency = c.powerEfficiency / n end + return c +end + +E.chip.fileHandle = lib.class { + __name = 'starsoul_electronics:chip.fileHandle'; + construct = function(chip, inode) -- stack, int --> fd + return { chip = chip, inode = inode } + end; + __index = { + read = function(self) + local dat = E.chip.read(self.chip) + return dat.files[self.inode] + end; + write = function(self,data) + print('writing', self.chip, self.inode) + return E.chip.fileWrite(self.chip, self.inode, data) + end; + erase = function(self) + local dat = E.chip.read(self.chip) + table.remove(dat.files, self.inode) + E.chip.write(self.chip, dat) + self.inode = nil + end; + open = function(self,fn) + return E.chip.fileOpen(self.chip, self.inode, fn) + end; + }; +} + +function E.chip.usableSoftware(chips,pgm) + local comp = E.chip.sumCompute(chips) + local r = {} + local unusable = {} + local sw if pgm then + if type(pgm) == 'string' then + pgm = {starsoul.item.sw.db[pgm]} + end + sw = pgm + else + sw = {} + for i, e in ipairs(chips) do + if (not e:is_empty()) + and minetest.get_item_group(e:get_name(), 'chip') ~= 0 + then + for fl, inode in E.chip.files(e) do + if fl.kind == 'sw' then + local s = starsoul.item.sw.db[fl.body.pgmId] + table.insert(sw, { + sw = s, chip = e, chipSlot = i; + file = fl, inode = inode; + }) + end + end + end + end + end + + for _, s in pairs(sw) do + if s.sw.cost.ram <= comp.ram then + table.insert(r, { + sw = s.sw; + chip = s.chip, chipSlot = s.chipSlot; + file = s.file; + fd = E.chip.fileHandle(s.chip, s.inode); + speed = s.sw.cost.cycles / comp.cycles; + powerCost = s.sw.cost.cycles / comp.powerEfficiency; + comp = comp; + }) + else + table.insert(unusable, { + sw = s.sw; + chip = s.chip; + ramNeeded = s.sw.cost.ram - comp.ram; + }) + end + end + return r, unusable +end + +starsoul.include 'sw' ADDED mods/starsoul-electronics/mod.conf Index: mods/starsoul-electronics/mod.conf ================================================================== --- mods/starsoul-electronics/mod.conf +++ mods/starsoul-electronics/mod.conf @@ -0,0 +1,3 @@ +name = starsoul_electronics +description = basic electronic components and logic +depends = starsoul ADDED mods/starsoul-electronics/sw.lua Index: mods/starsoul-electronics/sw.lua ================================================================== --- mods/starsoul-electronics/sw.lua +++ mods/starsoul-electronics/sw.lua @@ -0,0 +1,273 @@ +-- [ʞ] sw.lua +-- ~ lexi hale +-- 🄯 EUPL v1.2 +-- ? + +------------------------------- +-- basic suit nano abilities -- +------------------------------- +local function shredder(prop) + local function getItemsForFab(fab) + local elt + if fab then + elt = fab:elementalize() + else + elt = {} + end + local items = {} + if elt.element then + for k,v in pairs(elt.element) do + local st = ItemStack { + name = starsoul.world.material.element.db[k].form.element; + count = v; + } + table.insert(items, st) + end + end + return items + end + + return function(user, ctx) + local function cleanup() + user.action.prog.shred = nil + if user.action.sfx.shred then + minetest.sound_fade(user.action.sfx.shred, 1, 0) + user.action.sfx.shred = nil + end + if user.action.fx.shred then + user.action.fx.shred.abort() + end + end + + if user.action.tgt.type ~= 'node' then return end + local what = user.action.tgt.under + if what == nil or user.entity:get_pos():distance(what) > prop.range then + cleanup() + return false + end + local shredTime = 1.0 + local soundPitch = 1.0 -- TODO + local pdraw = prop.powerDraw or 0 + + local node = minetest.get_node(what) + local nd = minetest.registered_nodes[node.name] + local elt, fab, vary + if nd._starsoul then + fab = nd._starsoul.recover or nd._starsoul.fab + vary = nd._starsoul.recover_vary + end + if fab then + if fab.flag then + if fab.flag.unshreddable then + cleanup() + return false + -- TODO error beep + end + end + shredTime = fab.time and fab.time.shred or shredTime -- FIXME + if fab.cost and fab.cost.shredPower then + pdraw = pdraw * fab.cost.shredPower + end + end + local maxW = user:getSuit():maxPowerUse() + if maxW < pdraw then + shredTime = shredTime * (pdraw/maxW) + pdraw = maxW + end + if ctx.how.state == 'prog' then + local pdx = pdraw * ctx.how.delta + local p = user:suitDrawCurrent(pdx, ctx.how.delta, {kind='nano',label='Shredder'}, pdx) + if p < pdx then + cleanup() + return false + elseif not user.action.prog.shred then + cleanup() -- kill danglers + -- begin + user.action.prog.shred = 0 + user.action.sfx.shred = minetest.sound_play('starsoul-nano-shred', { + object = user.entity; + max_hear_distance = prop.range*2; + loop = true; + pitch = soundPitch; + }) + user.action.fx.shred = starsoul.fx.nano.shred(user, what, prop, shredTime, node) + else + user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0 + end + --print('shred progress: ', user.action.prog.shred) + if user.action.prog.shred >= shredTime then + if minetest.dig_node(what) then + --print('shred complete') + user:suitSound 'starsoul-success' + if fab then + local vf = fab + if vary then + local rng = (starsoul.world.seedbank+0xa891f62)[minetest.hash_node_position(what)] + vf = vf + vary(rng, {}) + end + local items = getItemsForFab(vf) + for i, it in ipairs(items) do user:give(it) end + end + else + user:suitSound 'starsoul-error' + end + cleanup() + end + elseif ctx.how.state == 'halt' then + cleanup() + end + return true + end +end + +starsoul.item.sw.link('starsoul_electronics:shred', { + name = 'NanoShred'; + kind = 'suitPower', powerKind = 'active'; + desc = 'An open-source program used in its various forks and iterations all across human-inhabited space and beyond. Rumored to contain fragments of code stolen from the nanoware of the Greater Races by an elusive infoterrorist.'; + size = 500e3; + cost = { + cycles = 100e6; + ram = 500e6; + }; + run = shredder{range=2, powerDraw=200}; +}) + +starsoul.item.sw.link('starsoul_electronics:compile_commune', { + name = 'Compile Matter'; + kind = 'suitPower', powerKind = 'direct'; + desc = "A basic suit matter compiler program, rather slow but ruthlessly optimized for power- and memory-efficiency by some of the Commune's most fanatic coders."; + size = 700e3; + cost = { + cycles = 300e6; + ram = 2e9; + }; + ui = 'starsoul:compile-matter-component'; + run = function(user, ctx) + end; +}) + +starsoul.item.sw.link('starsoul_electronics:compile_block_commune', { + name = 'Compile Block'; + kind = 'suitPower', powerKind = 'active'; + desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world."; + size = 5e6; + cost = { + cycles = 700e6; + ram = 4e9; + }; + ui = 'starsoul:compile-matter-block'; + run = function(user, ctx) + end; +}) + +do local J = starsoul.store.compilerJob + starsoul.item.sw.link('starsoul_electronics:driver_compiler_commune', { + name = 'Matter Compiler'; + kind = 'driver'; + desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle."; + size = 850e3; + cost = { + cycles = 400e6; + ram = 2e9; + }; + ui = 'starsoul:device-compile-matter-component'; + run = function(user, ctx) + end; + bgProc = function(user, ctx, interval, runState) + if runState.flags.compiled == true then return false end + -- only so many nanides to go around + runState.flags.compiled = true + local time = minetest.get_gametime() + local cyclesLeft = ctx.comp.cycles * interval + + for id, e in ipairs(ctx.file.body.conf) do + if e.key == 'job' then + local t = J.dec(e.value) + local remove = false + local r = starsoul.item.sw.db[t.schematic] + if not r then -- bad schematic + remove = true + else + local ccost = ctx.sw.cost.cycles + r.cost.cycles + local tcost = ccost / cyclesLeft + t.progress = t.progress + (1/tcost)*interval + cyclesLeft = cyclesLeft - ccost*interval + if t.progress >= 1 then + -- complete + remove = true + local i = starsoul.item.mk(r.output, { + how = 'print'; + user = user; -- for suit + compiler = { + node = ctx.compiler; -- for device + sw = ctx.sw; + install = ctx.fd; + }; + schematic = r; + }) + ctx.giveItem(i) + end + end + if remove then + table.remove(ctx.file.body.conf, id) + else + e.value = J.enc(t) + end + if not cyclesLeft > 0 then break end + end + end + ctx.saveConf() + end; + }) +end + +local function pasv_heal(effect, energy, lvl, pgmId) + return function(user, ctx, interval, runState) + if runState.flags.healed == true then return false end + -- competing nanosurgical programs?? VERY bad idea + runState.flags.healed = true + + local amt, f = user:effectiveStat 'health' + local st = user:getSuit():powerState() + if (st == 'on' and f < lvl) or (st == 'powerSave' and f < math.min(lvl,0.25)) then + local maxPower = energy*interval + local p = user:suitDrawCurrent(maxPower, interval, { + id = 'heal'; + src = 'suitPower'; + pgmId = pgmId; + healAmount = effect; + }) + if p > 0 then + local heal = (p/maxPower) * ctx.speed * effect*interval + --user:statDelta('health', math.max(1, heal)) + starsoul.fx.nano.heal(user, {{player=user.entity}}, heal, 1) + return true + end + end + return false -- program did not run + end; +end + +starsoul.item.sw.link('starsoul_electronics:nanomed', { + name = 'NanoMed'; + kind = 'suitPower', powerKind = 'passive'; + desc = 'Repair of the body is a Commune specialty, and their environment suits all come equipped with highly sophisticated nanomedicine suites, able to repair even the most grievous of wounds given sufficient energy input and time.'; + size = 2e9; + cost = { + cycles = 400e6; + ram = 3e9; + }; + run = pasv_heal(2, 20, 1); +}) + +starsoul.item.sw.link('starsoul_electronics:autodoc_deluxe', { + name = 'AutoDoc Deluxe'; + kind = 'suitPower', powerKind = 'passive'; + desc = "A flagship offering of the Excellence Unyielding nanoware division, AutoDoc Deluxe has been the top-rated nanocare package in the Celestial Shores Province for six centuries and counting. Every chip includes our comprehensive database of illnesses, prosyn schematics, and organ repair techniques, with free over-the-ether updates guaranteed for ten solariads from date of purchase! When professional medical care just isn't an option, 9/10 doctors recommend Excellence Unyielding AutoDoc Deluxe! The remaining doctor was bribed by our competitors."; + size = 1e9; + cost = { + cycles = 700e6; + ram = 1e9; + }; + run = pasv_heal(4, 50, .7); +}) ADDED mods/starsoul-material/elements.lua Index: mods/starsoul-material/elements.lua ================================================================== --- mods/starsoul-material/elements.lua +++ mods/starsoul-material/elements.lua @@ -0,0 +1,121 @@ +local lib = starsoul.mod.lib +local W = starsoul.world +local M = W.material + +M.element.meld { + hydrogen = { + name = 'hydrogen', sym = 'H', n = 1; + gas = true; + color = lib.color(1,0.8,.3); + }; + beryllium = { + name = 'Beryllium', sym = 'Be', n = 4; + metal = true; -- rare emerald-stuff + color = lib.color(0.2,1,0.2); + }; + oxygen = { + name = 'oxygen', sym = 'O', n = 8; + gas = true; + color = lib.color(.2,1,.2); + }; + carbon = { + name = 'carbon', sym = 'C', n = 6; + color = lib.color(.7,.2,.1); + }; + silicon = { + name = 'silicon', sym = 'Si', n = 14; + metal = true; -- can be forged into an ingot + color = lib.color(.6,.6,.4); + }; + potassium = { + name = 'potassium', sym = 'K', n = 19; + -- potassium is technically a metal but it's so soft + -- it can be easily nanoworked without high temps, so + -- ingots make no sense + color = lib.color(1,.8,0.1); + }; + calcium = { + name = 'calcium', sym = 'Ca', n = 20; + metal = true; + color = lib.color(1,1,0.7); + }; + aluminum = { + name = 'aluminum', sym = 'Al', n = 13; + metal = true; + color = lib.color(0.9,.95,1); + }; + iron = { + name = 'iron', sym = 'Fe', n = 26; + metal = true; + color = lib.color(.3,.3,.3); + }; + copper = { + name = 'copper', sym = 'Cu', n = 29; + metal = true; + color = lib.color(.8,.4,.1); + }; + lithium = { + name = 'lithium', sym = 'Li', n = 3; + -- i think lithium is considered a metal but we don't mark it as + -- one here because making a 'lithium ingot' is insane (even possible?) + color = lib.color(1,0.8,.3); + }; + titanium = { + name = 'titanium', sym = 'Ti', n = 22; + metal = true; + color = lib.color(.7,.7,.7); + }; + vanadium = { + name = 'vanadium', sym = 'V', n = 23; + metal = true; + color = lib.color(.3,0.5,.3); + }; + xenon = { + name = 'xenon', sym = 'Xe', n = 54; + gas = true; + color = lib.color(.5,.1,1); + }; + argon = { + name = 'argon', sym = 'Ar', n = 18; + gas = true; + color = lib.color(0,0.1,.9); + }; + osmium = { + name = 'osmium', sym = 'Os', n = 76; + metal = true; + color = lib.color(.8,.1,1); + }; + iridium = { + name = 'iridium', sym = 'Ir', n = 77; + metal = true; + color = lib.color(.8,0,.5); + }; + technetium = { + name = 'technetium', sym = 'Tc', n = 43; + desc = 'Prized by the higher Powers for subtle interactions that elude mere human scholars, technetium is of particular use in nuclear nanobatteries.'; + metal = true; + color = lib.color(.2,0.2,1); + }; + uranium = { + name = 'uranium', sym = 'U', n = 92; + desc = 'A weak but relatively plentiful nuclear fuel.'; + metal = true; + color = lib.color(.2,.7,0); + }; + thorium = { + name = 'thorium', sym = 'Th', n = 90; + desc = 'A frighteningly powerful nuclear fuel.'; + metal = true; + color = lib.color(.7,.3,.1); + }; + silver = { + name = 'silver', sym = 'Ag', n = 47; + metal = true; + color = lib.color(.7,.7,.8); + }; + gold = { + name = 'gold', sym = 'Au', n = 79; + metal = true; + color = lib.color(1,.8,0); + }; +} ADDED mods/starsoul-material/init.lua Index: mods/starsoul-material/init.lua ================================================================== --- mods/starsoul-material/init.lua +++ mods/starsoul-material/init.lua @@ -0,0 +1,22 @@ +local lib = starsoul.mod.lib +local M = { + canisterSizes = lib.registry.mk 'starsoul_material:canister-size'; +} +M.canisterSizes.foreach('starsoul_material:canister_link', {}, function(id, sz) + starsoul.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, { + name = sz.name; + slots = sz.slots; + vol = 0.1; -- too big for suit? + desc = sz.desc; + }) +end) +M.canisterSizes.meld { + tiny = {name = 'Tiny Canister', slots = 1, vol = 0.05}; + small = {name = 'Small Canister', slots = 3, vol = 0.2}; + mid = {name = 'Canister', slots = 5, vol = 0.5}; + large = {name = 'Large Canister', slots = 10, vol = 1.0}; + storage = {name = 'Storage Canister', slots = 50, vol = 5.0}; +} + +starsoul.include 'elements' + ADDED mods/starsoul-material/mod.conf Index: mods/starsoul-material/mod.conf ================================================================== --- mods/starsoul-material/mod.conf +++ mods/starsoul-material/mod.conf @@ -0,0 +1,3 @@ +name = starsoul_material +description = defines the raw materials and alloys used in printing +depends = starsoul ADDED mods/starsoul-scenario/init.lua Index: mods/starsoul-scenario/init.lua ================================================================== --- mods/starsoul-scenario/init.lua +++ mods/starsoul-scenario/init.lua @@ -0,0 +1,192 @@ +local lib = starsoul.mod.lib +local scenario = starsoul.world.scenario + +local function makeChip(label, schem, sw) + local E = starsoul.mod.electronics + local files = {} + local sz = 0 + for _, e in ipairs(schem) do + local p = E.sw.findSchematicFor(e[1]) + if p then + local file = { + kind = 'sw', name = '', drm = e[2]; + body = {pgmId = p}; + } + table.insert(files, file) + sz = sz + E.chip.fileSize(file) + end + end + for _, e in ipairs(sw) do + local file = { + kind = 'sw', name = '', drm = e[2]; + body = {pgmId = e[1]}; + } + table.insert(files, file) + sz = sz + E.chip.fileSize(file) + end + local chip = ItemStack(assert(E.chip.findBest(function(c) + return c.flash >= sz, c.ram + c.clockRate + end))) + local r = E.chip.read(chip) + r.label = label + r.files = files + E.chip.write(chip, r) + return chip +end + +local chipLibrary = { + compendium = makeChip('The Gentleman Adventurer\'s Compleat Wilderness Compendium', { + {'starsoul_electronics:battery_chemical_imperial_small', 0}; + }, { + {'starsoul_electronics:shred', 0}; + --{'starsoul_electronics:compile_empire', 0}; + {'starsoul_electronics:autodoc_deluxe', 1}; + --{'starsoul_electronics:driver_compiler_empire', 0}; + }); + survivalware = makeChip('Emergency Survivalware', { + {'starsoul_electronics:battery_chemical_commune_small', 0}; + }, { + {'starsoul_electronics:shred', 0}; + {'starsoul_electronics:compile_commune', 0}; + {'starsoul_electronics:nanomed', 0}; + {'starsoul_electronics:driver_compiler_commune', 0}; + }); + misfortune = makeChip("Sold1er0fMisf0rtune TOP Schematic Crackz REPACK", { + {'starsoul_electronics:battery_chemical_usukwinya_mid', 0}; + {'starsoul_electronics:battery_hybrid_imperial_small', 0}; + -- ammunition + }, {}); +} + + +table.insert(scenario, { + id = 'starsoul_scenario:imperialExpat'; + name = 'Imperial Expat'; + desc = "Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of some twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.\nAt least you got some nifty psionic powers out of this whole clusterfuck. Hopefully they're safe to use."; + + species = 'human'; + speciesVariant = 'female'; + soul = { + externalChannel = true; -- able to touch other souls in the spiritual realm + physicalChannel = true; -- able to extend influence into physical realm + damage = 1; + }; + social = { + empire = 'workingClass'; + commune = 'metic'; + }; + + startingItems = { + suit = ItemStack('starsoul_suit:suit_survival_commune'); + suitBatteries = {ItemStack('starsoul_electronics:battery_carbon_commune_small')}; + suitChips = { + chipLibrary.survivalware; + -- you didn't notice it earlier, but your Commune environment suit + -- came with this chip already plugged in. it's apparently true + -- what they say: the Commune is always prepared for everything. + -- E V E R Y T H I N G. + }; + suitGuns = {}; + suitAmmo = {}; + suitCans = { + ItemStack('starsoul_material:canister_small'); + }; + carry = { + chipLibrary.compendium; + -- you bought this on a whim before you left the Empire, and + -- just happened to still have it on your person when everything + -- went straight to the Wild Gods' privy + }; + }; +}) + +table.insert(scenario, { + id = 'starsoul_scenario:gentlemanAdventurer'; + -- Othar Tryggvasson, + name = 'Gentleman Adventurer'; + desc = "Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now *that* should pop his monocle -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal. How exciting -- a genuine frontier outing!"; + + species = 'human'; + speciesVariant = 'male'; + soul = { + externalChannel = true; + physicalChannel = true; + damage = 1; + }; + social = { + empire = 'lord'; + }; + + startingItems = { + suit = 'starsoul_suit:suit_survival_imperial'; + suitBatteries = {ItemStack('starsoul_electronics:battery_supercapacitor_imperial_mid')}; + suitChips = { + chipLibrary.compendium; + -- Mother, bless her soul, simply insisted on buying you this as a parting + -- gift. "it's dangerous out there for a young man," she proclaimed as + -- if she had profound firsthand experience of the matter. mindful of the + -- husband she endures, you suffered to humor her, and made a big show of + -- installing it your brand-new nanosuit before you fled the family seat. + }; + suitGuns = {}; + suitAmmo = {}; + suitCans = { + ItemStack('starsoul_material:canister_mid'); + }; + carry = {}; + }; +}) + +table.insert(scenario, { + -- you start out with strong combat abilities but weak engineering, + -- and will have to scavenge wrecks to find basic crafting gear + id = 'starsoul_scenario:terroristTagalong'; + name = 'Terrorist Tagalong'; + desc = "It turns out there's a *reason* Crown jobs pay so well."; + species = 'human'; + speciesVariant = 'female'; + social = { + empire = 'lowlife'; + commune = 'mostWanted'; + underworldConnections = true; + }; + soul = { + externalChannel = true; + physicalChannel = true; + damage = 2; -- closer to the blast + }; + startingItems = { + suit = 'starsoul_suit:suit_combat_imperial'; + suitBatteries = { + ItemStack('starsoul_electronics:battery_supercapacitor_imperial_small'); + ItemStack('starsoul_electronics:battery_chemical_imperial_large'); + }; + suitGuns = {}; + suitAmmo = {}; + carry = {}; + }; + suitChips = {chipLibrary.misfortune}; +}) + +table.insert(scenario, { + id = 'starsoul_scenario:tradebirdBodyguard'; + name = 'Tradebird Bodyguard'; + desc = "You've never understood why astropaths of all people *insist* on bodyguards. This one could probably make hash of a good-sized human batallion, if her feathers were sufficiently ruffled. Perhaps it's a status thing. Whatever the case, it's easy money.\nAt least, it was supposed to be.'"; + species = 'usukwinya'; + speciesVariant = 'male'; + soul = { + damage = 0; -- Inyukiriku and her entourage fled the ship when she sensed something serious was about to go down. lucky: the humans only survived because their souls were closed off to the Physical. less luckily, the explosion knocked your escape pod off course and the damn astropath was too busy saving her own skin to come after you + externalChannel = true; -- usukwinya are already psionic + physicalChannel = true; -- usukwinya are Starsouls + }; + startingItems = { + suit = 'starsoul_suit:suit_combat_usukwinya'; + suitBatteries = { + ItemStack('starsoul_electronics:battery_hybrid_usukwinya_mid'); + }; + suitGuns = {}; + suitChips = {}; + suitAmmo = {}; + carry = {}; + }; +}) ADDED mods/starsoul-scenario/mod.conf Index: mods/starsoul-scenario/mod.conf ================================================================== --- mods/starsoul-scenario/mod.conf +++ mods/starsoul-scenario/mod.conf @@ -0,0 +1,4 @@ +name = starsoul_scenario +description = built-in scenarios for Starsoul +depends = starsoul, starsoul_suit, starsoul_electronics, starsoul_building, starsoul_material +# be sure to add any mods from which you list new starting items! ADDED mods/starsoul-secrets/init.lua Index: mods/starsoul-secrets/init.lua ================================================================== --- mods/starsoul-secrets/init.lua +++ mods/starsoul-secrets/init.lua @@ -0,0 +1,58 @@ +---------------------------------------------------- +------------- CONTROLLED INFORMATION -------------- +---------------------------------------------------- +-- THE INFORMATION CONTAINED IN THIS DOCUMENT IS -- +-- SUBJECT TO RESTRAINT OF TRANSMISSION PER THE -- +-- TERMS OF THE COMMUNE CHARTER INFOSECURITY -- +-- PROVISION. IF YOU ARE NOT AUTHORIZED UNDER THE -- +-- AEGIS OF THE APPROPRITE CONTROLLING AUTHORITY, -- +-- CLOSE THIS DOCUMENT IMMEDIATELY AND REPORT THE -- +-- SECURITY BREACH TO YOUR DESIGNATED INFORMATION -- +-- HYGIENE OVERSEER OR FACE CORRECTIVE DISCIPLINE -- +---------------------------------------------------- + +local lib = starsoul.mod.lib +local sec = {} +starsoul.mod.secrets = sec + +sec.index = lib.registry.mk 'starsoul_secrets:secret' + +--[==[ + +a secret is a piece of information that is made available +for review once certain conditions are met. despite the name, +it doesn't necessarily have to be secret -- it could include +e.g. journal entries about a character's background. a secret +is defined in the following manner: + +{ + title = the string that appears in the UI + stages = { + { + prereqs = { + {kind = 'fact', id = 'starsoul:terroristEmployer'} + {kind = 'item', id = 'starsoul_electronic:firstbornDoomBong'} + {kind = 'background', id = 'starsoul:terroristTagalong'} + } + body = { + 'the firstborn smonked hella weed'; + }; + -- body can also be a function(user,secret) + } + } +} + +TODO would it be useful to impl horn clauses and a general fact database? + is that level of flexibility meaningful? or are simply flags better + +a secret can be a single piece of information predicated +on a fact, in which case the secret and fact should share +the same ID. the ID should be as non-indicative as possible +to avoid spoilers for devs of unrelated code. + +a secret can also be manually unlocked e.g. by using an item + +]==]-- + +function sec.prereqCheck(user, pr) +end ADDED mods/starsoul-secrets/mod.conf Index: mods/starsoul-secrets/mod.conf ================================================================== --- mods/starsoul-secrets/mod.conf +++ mods/starsoul-secrets/mod.conf @@ -0,0 +1,4 @@ +name = starsoul_secrets +title = starsoul secrets +description = TS//NOFORN +depends = starsoul ADDED mods/starsoul-suit/init.lua Index: mods/starsoul-suit/init.lua ================================================================== --- mods/starsoul-suit/init.lua +++ mods/starsoul-suit/init.lua @@ -0,0 +1,180 @@ +local lib = starsoul.mod.lib +local fab = starsoul.type.fab + +local facDescs = { + commune = { + survival = { + suit = 'A light, simple, bare-bones environment suit that will provide heating, cooling, and nanide support to a stranded cosmonaut'; + cc = 'The Survival Suit uses compact thermoelectrics to keep the wearer perfectly comfortable in extremes of heat or cold. It makes up for its heavy power usage with effective insulation that substantially reduces any need for climate control.'; + }; + engineer = { + suit = 'A lightweight environment suit designed for indoor work, the Commune\'s Engineer Suit boasts advanced nanotech capable of constructing objects in place.'; + cc = 'The Engineer Suit is designed for indoor work. Consequently, it features only a low-power thermoelectric cooler meant to keep its wearer comfortable during strenuous work.'; + }; + combat = { + suit = 'A military-grade suit with the latest Commune technology. Designed for maximum force multiplication, the suit has dual weapon hardpoints and supports a gargantuan power reserve. Its nanotech systems are specialized for tearing through obstacles, but can also be used to manufacturer ammunition in a pinch.'; + cc = 'This Combat Suit uses electrothermal cooling to keep an active soldier comfortable and effective, as well as conventional heating coils to enable operation in hostile atmospheres.'; + }; + }; + +} + + +starsoul.world.tier.foreach('starsoul:suit-gen', {}, function(tid, t) + local function hasTech(tech) + return starsoul.world.tier.tech(tid, tech) + end + if not hasTech 'suit' then return end + -- TODO tier customization + -- + local function fabsum(f) + return starsoul.world.tier.fabsum(tid, 'suit') + end + local function fabReq(sz, days) + local tierMatBase = ( + (fabsum 'electric' * 4) + + (fabsum 'basis' + fabsum 'suit') * sz + ) + local b = tierMatBase + fab { + -- universal suit requirements + time = { print = 60*60*24 * days }; + size = { printBay = sz }; + } + b.flag = lib.tbl.set('print'); + return b + end + local function facDesc(s, t) + local default = 'A protective nanosuit' -- FIXME + if not facDescs[tid] then return default end + if not facDescs[tid][s] then return default end + if not facDescs[tid][s][t] then return default end + return facDescs[tid][s][t] + end + starsoul.item.suit.link('starsoul_suit:suit_survival_' .. tid, { + name = t.name .. ' Survival Suit'; + desc = facDesc('survival','suit'); + fab = fabReq(1, 2.2) + fab { }; + tex = { + plate = { + id = 'starsoul-suit-survival-plate'; + tint = lib.color {hue = 210, sat = .5, lum = .5}; + }; + lining = { + id = 'starsoul-suit-survival-lining'; + tint = lib.color {hue = 180, sat = .2, lum = .7}; + }; + }; + tints = {'suit_plate', 'suit_lining'}; + temp = { + desc = facDesc('survival','cc'); + maxHeat = 0.7; -- can produce a half-degree Δ per second + maxCool = 0.5; + heatPower = 50; -- 50W + coolPower = 50/t.efficiency; + insulation = 0.5; -- prevent half of heat loss + }; + protection = { + rad = 0.7; -- blocks 70% of ionizing radiation + }; + slots = { + canisters = 1; + batteries = math.ceil(math.max(1, t.power/2)); + chips = 3; + guns = 0; + ammo = 0; + }; + nano = { + compileSpeed = 0.1 * t.efficiency; + shredSpeed = 0.1 * t.power; + fabSizeLimit = 0.6; -- 60cm + }; + }) + + starsoul.item.suit.link('starsoul_suit:suit_engineer_' .. tid, { + name = t.name .. ' Engineer Suit'; + desc = facDesc('engineer','suit'); + tex = { + plate = { + id = 'starsoul-suit-survival-plate'; + tint = lib.color {hue = 0, sat = .5, lum = .7}; + }; + }; + tints = {'suit_plate', 'suit_lining'}; + fab = fabReq(.8, 7) + fab { }; + temp = { + desc = facDesc('engineer','cc'); + maxHeat = 0; + maxCool = 0.2; + heatPower = 0; + coolPower = 10 / t.efficiency; + insulation = 0.1; -- no lining + }; + slots = { + canisters = 2; + batteries = 2; + chips = 6; + guns = 0; + ammo = 0; + }; + compat = { + maxBatterySize = 0.10 * t.power; -- 10cm + }; + protection = { + rad = 0.1; -- blocks 10% of ionizing radiation + }; + nano = { + compileSpeed = 1 * t.efficiency; + shredSpeed = 0.7 * t.power; + fabSizeLimit = 1.5; -- 1.5m (enables node compilation) + }; + }) + + if hasTech 'suitCombat' then + starsoul.item.suit.link('starsoul_suit:suit_combat_' .. tid, { + name = t.name .. ' Combat Suit'; + desc = facDesc('combat','suit'); + fab = fabReq(1.5, 14) + fab { + metal = {iridium = 1e3}; + }; + tex = { + plate = { + id = 'starsoul-suit-survival-plate'; + tint = lib.color {hue = 0, sat = 0, lum = 0}; + }; + lining = { + id = 'starsoul-suit-survival-lining'; + tint = lib.color {hue = 180, sat = .5, lum = .3}; + }; + }; + tints = {'suit_plate', 'suit_lining'}; + slots = { + canisters = 1; + batteries = math.ceil(math.max(3, 8*(t.power/2))); + chips = 5; + guns = 2; + ammo = 1; + }; + compat = { + maxBatterySize = 0.10 * t.power; -- 10cm + }; + temp = { + desc = facDesc('combat','cc'); + maxHeat = 0.3; + maxCool = 0.6; + heatPower = 20 / t.efficiency; + coolPower = 40 / t.efficiency; + insulation = 0.2; + }; + protection = { + rad = 0.9; -- blocks 90% of ionizing radiation + }; + nano = { + compileSpeed = 0.05; + shredSpeed = 2 * t.power; + fabSizeLimit = 0.3; -- 30cm + }; + }) + end +end) + + ADDED mods/starsoul-suit/mod.conf Index: mods/starsoul-suit/mod.conf ================================================================== --- mods/starsoul-suit/mod.conf +++ mods/starsoul-suit/mod.conf @@ -0,0 +1,3 @@ +name = starsoul_suit +description = defines the environment suits available in starsoul +depends = starsoul, starsoul_electronics ADDED mods/starsoul/container.lua Index: mods/starsoul/container.lua ================================================================== --- mods/starsoul/container.lua +++ mods/starsoul/container.lua @@ -0,0 +1,91 @@ +-- a container item defines a 'container' structure listing its +-- inventories and their properties. a container object is created +-- in order to interact with a container +local lib = starsoul.mod.lib +starsoul.item.container = lib.class { + __name = 'starsoul:container'; + construct = function(stack, inv, def) + local T,G = lib.marshal.t, lib.marshal.g + local cdef = stack:get_definition()._starsoul.container; + local sd = {} + for k,v in pairs(cdef.list) do + sd[k] = { + key = v.key; + type = T.inventoryList; + } + end + return { + stack = stack, inv = inv, pdef = def, cdef = cdef; + store = lib.marshal.metaStore(sd)(stack); + } + end; + __index = { + slot = function(self, id) + return string.format("%s_%s", self.pdef.pfx, id) + end; + clear = function(self) -- initialize or empty the metadata + self:update(function() + for k,v in pairs(self.cdef.list) do + if v.sz > 0 then + self.store.write(k, {}) + end + end + end) + end; + list = function(self, k) return self.store.read(k) end; + read = function(self) + local lst = {} + for k,v in pairs(self.cdef.list) do + if v.sz > 0 then lst[k] = self:list(k) end + end + return lst + end; + pull = function(self) -- align the inventories with the metadata + for k,v in pairs(self.cdef.list) do + if v.sz > 0 then + local stacks = self:list(k) + local sid = self:slot(k) + self.inv:set_size(sid, v.sz) + self.inv:set_list(sid, stacks) + end + end + end; + update = function(self, fn) + local old = ItemStack(self.stack) + if fn then fn() end + if self.cdef.handle then + self.cdef.handle(self.stack, old) + end + end; + push = function(self) -- align the metadata with the inventories + self:update(function() + for k,v in pairs(self.cdef.list) do + if v.sz > 0 then + local sid = self:slot(k) + local lst = self.inv:get_list(sid) + self.store.write(k, lst) + end + end + end) + end; + drop = function(self) -- remove the inventories from the node/entity + for k,v in pairs(self.cdef.list) do + local sid = self:slot(k) + self.inv:set_size(sid, 0) + end + end; + slotAccepts = function(self, lst, slot, stack) + end; + }; +} + +function starsoul.item.container.dropPrefix(inv, pfx) + local lists = inv:get_lists() + for k,v in pairs(lists) do + if #k > #pfx then + if string.sub(k, 1, #pfx + 1) == pfx .. '_' then + inv:set_size(k, 0) + end + end + end +end ADDED mods/starsoul/effect.lua Index: mods/starsoul/effect.lua ================================================================== --- mods/starsoul/effect.lua +++ mods/starsoul/effect.lua @@ -0,0 +1,402 @@ +-- ported from sorcery/spell.lua, hence the lingering refs to "magic" +-- +-- this file is used to track active effects, for the purposes of metamagic +-- like disjunction. a "effect" is a table consisting of several properties: +-- a "disjoin" function that, if present, is called when the effect is +-- abnormally interrupted, a "terminate" function that calls when the effect +-- completes, a "duration" property specifying how long the effect lasts in +-- seconds, and a "timeline" table that maps floats to functions called at +-- specific points during the function's activity. it can also have a +-- 'delay' property that specifies how long to wait until the effect sequence +-- starts; the effect is however still vulnerable to disjunction during this +-- period. there can also be a sounds table that maps timepoints to sounds +-- the same way timeline does. each value should be a table of form {sound, +-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or +-- a vector specifying a position in the world, and indicate where the sound +-- should be played. by default 'caster' and 'subjects' sounds will be attached +-- to the objects they reference; 'attach=false' can be added to prevent this. +-- by default sounds will be faded out quickly when disjunction occurs; this +-- can be controlled by the fade parameter. +-- +-- effects can have various other properties, for instance 'disjunction', which +-- when true prevents other effects from being cast in its radius while it is +-- still in effect. disjunction is absolute; there is no way to overwhelm it. +-- +-- the effect also needs at least one of "anchor", "subjects", or "caster". +-- * an anchor is a position that, in combination with 'range', specifies the area +-- where a effect is in effect; this is used for determining whether it +-- is affected by a disjunction that incorporates part of that position +-- * subjects is an array of individuals affected by the effect. when +-- disjunction is cast on one of them, they will be removed from the +-- table. each entry should have at least a 'player' field; they can +-- also contain any other data useful to the effect. if a subject has +-- a 'disjoin' field it must be a function called when they are removed +-- from the list of effect targets. +-- * caster is the individual who cast the effect, if any. a disjunction +-- against their person will totally disrupt the effect. +local log = starsoul.logger 'effect' +local lib = starsoul.mod.lib + +-- FIXME saving object refs is iffy, find a better alternative +starsoul.effect = { + active = {} +} + +local get_effect_positions = function(effect) + local effectpos + if effect.anchor then + effectpos = {effect.anchor} + elseif effect.attach then + if effect.attach == 'caster' then + effectpos = {effect.caster:get_pos()} + elseif effect.attach == 'subjects' or effect.attach == 'both' then + if effect.attach == 'both' then + effectpos = {effect.caster:get_pos()} + else effectpos = {} end + for _,s in pairs(effect.subjects) do + effectpos[#effectpos+1] = s.player:get_pos() + end + else effectpos = {effect.attach:get_pos()} end + else assert(false) end + return effectpos +end + +local ineffectrange = function(effect,pos,range) + local effectpos = get_effect_positions(effect) + + for _,p in pairs(effectpos) do + if vector.equals(pos,p) or + (range and lib.math.vdcomp(range, pos,p)<=1) or + (effect.range and lib.math.vdcomp(effect.range,p,pos)<=1) then + return true + end + end + return false +end + +starsoul.effect.probe = function(pos,range) + -- this should be called before any effects are performed. + -- other mods can overlay their own functions to e.g. protect areas + -- from effects + local result = {} + + -- first we need to check if any active injunctions are in effect + -- injunctions are registered as effects with a 'disjunction = true' + -- property + for id,effect in pairs(starsoul.effect.active) do + if not (effect.disjunction and (effect.anchor or effect.attach)) then goto skip end + if ineffectrange(effect,pos,range) then + result.disjunction = true + break + end + ::skip::end + + -- at some point we might also check to see if certain anti-effect + -- blocks are nearby or suchlike. there could also be regions where + -- perhaps certain kinds of effect are unusually empowered or weak + return result +end +starsoul.effect.disjoin = function(d) + local effects,targets = {},{} + if d.effect then effects = {{v=d.effect}} + elseif d.target then targets = {d.target} + elseif d.pos then -- find effects anchored here and people in range + for id,effect in pairs(starsoul.effect.active) do + if not effect.anchor then goto skip end -- this intentionally excludes attached effects + if ineffectrange(effect,d.pos,d.range) then + effects[#effects+1] = {v=effect,i=id} + end + ::skip::end + local ppl = minetest.get_objects_inside_radius(d.pos,d.range) + if #targets == 0 then targets = ppl else + for _,p in pairs(ppl) do targets[#targets+1] = p end + end + end + + -- iterate over targets to remove from any effect's influence + for _,t in pairs(targets) do + for id,effect in pairs(starsoul.effect.active) do + if effect.caster == t then effects[#effects+1] = {v=effect,i=id} else + for si, sub in pairs(effect.subjects) do + if sub.player == t then + if sub.disjoin then sub:disjoin(effect) end + effect.release_subject(si) + break + end + end + end + end + end + + -- effects to disjoin entirely + for _,s in pairs(effects) do local effect = s.v + if effect.disjoin then effect:disjoin() end + effect.abort() + if s.i then starsoul.effect.active[s.i] = nil else + for k,v in pairs(starsoul.effect.active) do + if v == effect then starsoul.effect.active[k] = nil break end + end + end + end +end + +starsoul.effect.ensorcelled = function(player,effect) + if type(player) == 'string' then player = minetest.get_player_by_name(player) end + for _,s in pairs(starsoul.effect.active) do + if effect and (s.name ~= effect) then goto skip end + for _,sub in pairs(s.subjects) do + if sub.player == player then return s end + end + ::skip::end + return false +end + +starsoul.effect.each = function(player,effect) + local idx = 0 + return function() + repeat idx = idx + 1 + local sp = starsoul.effect.active[idx] + if sp == nil then return nil end + if effect == nil or sp.name == effect then + for _,sub in pairs(sp.subjects) do + if sub.player == player then return sp end + end + end + until idx >= #starsoul.effect.active + end +end + +-- when a new effect is created, we analyze it and make the appropriate calls +-- to minetest.after to queue up the events. each job returned needs to be +-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling +-- necessary :D + +starsoul.effect.cast = function(proto) + local s = table.copy(proto) + s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {} + s.impacts = s.impacts or {} s.subjects = s.subjects or {} + s.delay = s.delay or 0 + s.visual = function(subj, def) + s.vfx[#s.vfx + 1] = { + handle = minetest.add_particlespawner(def); + subject = subj; + } + end + s.visual_caster = function(def) -- convenience function + local d = table.copy(def) + d.attached = s.caster + s.visual(nil, d) + end + s.visual_subjects = function(def) + for _,sub in pairs(s.subjects) do + local d = table.copy(def) + d.attached = sub.player + s.visual(sub, d) + end + end + s.affect = function(i) + local etbl = {} + for _,sub in pairs(s.subjects) do + -- local eff = late.new_effect(sub.player, i) + -- starsoul will not be using late + local rec = { + effect = eff; + subject = sub; + } + s.impacts[#s.impacts+1] = rec + etbl[#etbl+1] = rec + end + return etbl + end + s.abort = function() + for _,j in ipairs(s.jobs) do j:cancel() end + for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end + for _,i in ipairs(s.sfx) do s.silence(i) end + for _,i in ipairs(s.impacts) do i.effect:stop() end + end + s.release_subject = function(si) + local t = s.subjects[si] + for _,f in pairs(s.sfx) do if f.subject == t then s.silence(f) end end + for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end + for _,f in pairs(s.vfx) do + if f.subject == t then minetest.delete_particlespawner(f.handle) end + end + s.subjects[si] = nil + end + local interpret_timespec = function(when) + if when == nil then return 0 end + local t if type(when) == 'number' then + t = s.duration * when + else + t = (s.duration * (when.whence or 0)) + (when.secs or 0) + end + if t then return math.min(s.duration,math.max(0,t)) end + + log.err('invalid timespec ' .. dump(when)) + return 0 + end + s.queue = function(when,fn) + local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0 + local timepast = interpret_timespec(when) + if not timepast then timepast = 0 end + local timeleft = s.duration - timepast + local howlong = (s.delay + timepast) - elapsed + if howlong < 0 then + log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed') + howlong = 0 + end + s.jobs[#s.jobs+1] = minetest.after(howlong, function() + -- this is somewhat awkward. since we're using a non-polling approach, we + -- need to find a way to account for a caster or subject walking into an + -- existing antimagic field, or someone with an existing antimagic aura + -- walking into range of the anchor. so every time a effect effect would + -- take place, we first check to see if it's in range of something nasty + if not s.disjunction and -- avoid self-disjunction + ((s.caster and starsoul.effect.probe(s.caster:get_pos()).disjunction) or + (s.anchor and starsoul.effect.probe(s.anchor,s.range).disjunction)) then + starsoul.effect.disjoin{effect=s} + else + if not s.disjunction then for _,sub in pairs(s.subjects) do + local sp = sub.player:get_pos() + if starsoul.effect.probe(sp).disjunction then + starsoul.effect.disjoin{pos=sp} + end + end end + -- effect still exists and we've removed any subjects who have been + -- affected by a disjunction effect, it's now time to actually perform + -- the queued-up action + fn(s,timepast,timeleft) + end + end) + end + s.play_now = function(spec) + local specs, stbl = {}, {} + local addobj = function(obj,sub) + if spec.attach == false then specs[#specs+1] = { + spec = { pos = obj:get_pos() }; + obj = obj, subject = sub; + } else specs[#specs+1] = { + spec = { object = obj }; + obj = obj, subject = sub; + } end + end + + if spec.where == 'caster' then addobj(s.caster) + elseif spec.where == 'subjects' then + for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end + elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} } + else specs[#specs+1] = { spec = {pos = spec.where} } end + + for _,sp in pairs(specs) do + sp.spec.gain = sp.spec.gain or spec.gain + local so = { + handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral); + ctl = spec; + -- object = sp.obj; + subject = sp.subject; + } + stbl[#stbl+1] = so + s.sfx[#s.sfx+1] = so + end + return stbl + end + s.play = function(when,spec) + s.queue(when, function() + local snds = s.play_now(spec) + if spec.stop then + s.queue(spec.stop, function() + for _,snd in pairs(snds) do s.silence(snd) end + end) + end + end) + end + s.silence = function(sound) + if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle) + else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end + end + local startqueued, termqueued = false, false + local myid = #starsoul.effect.active+1 + s.cancel = function() + s.abort() + starsoul.effect.active[myid] = nil + end + local perform_disjunction_calls = function() + local positions = get_effect_positions(s) + for _,p in pairs(positions) do + starsoul.effect.disjoin{pos = p, range = s.range} + end + end + if s.timeline then + for when_raw,what in pairs(s.timeline) do + local when = interpret_timespec(when_raw) + if s.delay == 0 and when == 0 then + startqueued = true + if s.disjunction then perform_disjunction_calls() end + what(s,0,s.duration) + elseif when_raw == 1 or when >= s.duration then -- avoid race conditions + if not termqueued then + termqueued = true + s.queue(1,function(s,...) + what(s,...) + if s.terminate then s:terminate() end + starsoul.effect.active[myid] = nil + end) + else + log.warn('multiple final timeline events not possible, ignoring') + end + elseif when == 0 and s.disjunction then + startqueued = true + s.queue(when_raw,function(...) + perform_disjunction_calls() + what(...) + end) + else s.queue(when_raw,what) end + end + end + if s.intervals then + for _,int in pairs(s.intervals) do + local timeleft = s.duration - interpret_timespec(int.after) + local iteration, itercount = 0, timeleft / int.period + local function iterate(lastreturn) + iteration = iteration + 1 + local nr = int.fn { + effect = s; + iteration = iteration; + iterationcount = itercount; + timeleft = timeleft; + timeelapsed = s.duration - timeleft; + lastreturn = lastreturn; + } + if nr ~= false and iteration < itercount then + s.jobs[#s.jobs+1] = minetest.after(int.period, + function() iterate(nr) end) + end + end + if int.after + then s.queue(int.after, iterate) + else s.queue({whence=0, secs=s.period}, iterate) + end + end + end + if s.disjunction and not startqueued then + if s.delay == 0 then perform_disjunction_calls() else + s.queue(0, function() perform_disjunction_calls() end) + end + end + if s.sounds then + for when,what in pairs(s.sounds) do s.play(when,what) end + end + starsoul.effect.active[myid] = s + if not termqueued then + s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function() + if s.terminate then s:terminate() end + starsoul.effect.active[myid] = nil + end) + end + s.starttime = minetest.get_server_uptime() + return s +end + +minetest.register_on_dieplayer(function(player) + starsoul.effect.disjoin{target=player} +end) ADDED mods/starsoul/element.lua Index: mods/starsoul/element.lua ================================================================== --- mods/starsoul/element.lua +++ mods/starsoul/element.lua @@ -0,0 +1,218 @@ +local lib = starsoul.mod.lib +local W = starsoul.world +local M = W.material + +M.element.foreach('starsoul:sort', {}, function(id, m) + if m.metal then + M.metal.link(id, { + name = m.name; + composition = starsoul.type.fab{element = {[id] = 1}}; + color = m.color; + -- n.b. this is a RATIO: it will be appropriately multiplied + -- for the object in question; e.g a normal chunk will be + -- 100 $element, an ingot will be 1000 $element + }) + elseif m.gas then + M.gas.link(id, { + name = m.name; + composition = starsoul.type.fab{element = {[id] = 1}}; + }) + elseif m.liquid then + M.liquid.link(id, { + name = m.name; + composition = starsoul.type.fab{element = {[id] = 1}}; + }) + end +end) + +local F = string.format + +local function mkEltIndicator(composition) + local indicator = '' + local idx = 0 + local ccount = 0 + for _ in pairs(composition) do + ccount = ccount + 1 + end + local indsz,indpad = 28,4 + local ofs = math.min(11, (indsz-indpad)/ccount) + for id, amt in pairs(composition) do + idx = idx + 1 + indicator = indicator .. F( + ':%s,3=starsoul-element-%s.png', + (indsz-indpad) - (idx*ofs), id + ) + end + indicator = lib.image(indicator) + return function(s) + return string.format('(%s^[resize:%sx%s)^[combine:%sx%s%s', + s, + indsz, indsz, + indsz, indsz, + indicator); + end +end + +M.element.foreach('starsoul:gen-forms', {}, function(id, m) + local eltID = F('%s:element_%s', minetest.get_current_modname(), id) + local eltName = F('Elemental %s', lib.str.capitalize(m.name)) + local tt = function(t, d, g) + return starsoul.ui.tooltip { + title = t, desc = d; + color = lib.color(0.1,0.2,0.1); + props = { + {title = 'Mass', desc = lib.math.si('g', g), affinity='info'} + } + } + end + local comp = {[id] = 1} + local iblit = mkEltIndicator(comp) + m.form = m.form or {} + m.form.element = eltID + + local powder = F('starsoul-element-%s-powder.png', id); + minetest.register_craftitem(eltID, { + short_description = eltName; + description = tt(eltName, F('Elemental %s kept in suspension by a nanide storage system, ready to be worked by a cold matter compiler', m.name), 1); + inventory_image = iblit(powder); + wield_image = powder; + stack_max = 1000; -- 1kg + groups = {element = 1, powder = 1, specialInventory = 1}; + _starsoul = { + mass = 1; + material = { + kind = 'element'; + element = id; + }; + fab = starsoul.type.fab { + element = comp; + }; + }; + }); +end) + + +M.metal.foreach('starsoul:gen-forms', {}, function(id, m) + local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id) + local brickID, ingotID = baseID .. 'brick', baseID .. 'ingot' + local brickName, ingotName = + F('%s Brick', lib.str.capitalize(m.name)), + F('%s Ingot', lib.str.capitalize(m.name)) + m.form = m.form or {} + m.form.brick = brickID + m.form.ingot = ingotID + local tt = function(t, d, g) + return starsoul.ui.tooltip { + title = t, desc = d; + color = lib.color(0.1,0.1,0.1); + props = { + {title = 'Mass', desc = lib.math.si('g', g), affinity='info'} + } + } + end + local mcomp = m.composition:elementalize().element + local function comp(n) + local t = {} + for id, amt in pairs(mcomp) do + t[id] = amt * n + end + return t + end + local iblit = mkEltIndicator(mcomp) + local function img(s) + return iblit(s:colorize(m.color):render()) + end + + minetest.register_craftitem(brickID, { + short_description = brickName; + description = tt(brickName, F('A solid brick of %s, ready to be worked by a matter compiler', m.name), 100); + inventory_image = img(lib.image 'starsoul-item-brick.png'); + wield_image = lib.image 'starsoul-item-brick.png':colorize(m.color):render(); + stack_max = 10; + groups = {metal = 1, ingot = 1}; + _starsoul = { + mass = 100; + material = { + kind = 'metal'; + metal = id; + }; + fab = starsoul.type.fab { + flag = {smelt= true}; + element = comp(1e2); + }; + }; + }); + + minetest.register_craftitem(ingotID, { + short_description = ingotName; + description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3); + inventory_image = img(lib.image('starsoul-item-ingot.png')); + wield_image = lib.image 'starsoul-item-ingot.png':colorize(m.color):render(); + groups = {metal = 1, ingot = 1}; + stack_max = 5; + _starsoul = { + mass = 1e3; + material = { + kind = 'metal'; + metal = id; + }; + fab = starsoul.type.fab { + flag = {smelt= true}; + element = comp(1e3); + }; + }; + }); + + +end) + +local function canisterDesc(stack, def) + def = def or stack:get_definition()._starsoul.canister + local props = { + {title = 'Charge Slots', affinity = 'info', desc = tostring(def.slots)}; + }; + if stack then + local inv = starsoul.item.container(stack) + for i,e in ipairs(inv:list 'elem') do + local comp = e:get_definition()._starsoul.fab + table.insert(props, { + title = comp:formula(); + desc = lib.math.si('g', e:get_count()); + affinity = 'good'; + }) + end + -- TODO list masses + end + return starsoul.ui.tooltip { + title = def.name, desc = def.desc or 'A canister that can store a charge of elemental powder, gas, or liquid'; + color = lib.color(0.2,0.1,0.1); + props = props; + }; +end + +starsoul.item.canister = lib.registry.mk 'starsoul:canister'; +starsoul.item.canister.foreach('starsoul:item-gen', {}, function(id, c) + minetest.register_craftitem(id, { + short_description = c.name; + description = canisterDesc(nil, c); + inventory_image = c.image or 'starsoul-item-element-canister.png'; + groups = {canister = 1}; + stack_max = 1; + _starsoul = { + canister = c; + container = { + handle = function(stack, oldstack) + stack:get_meta():set_string('description', canisterDesc(stack)) + return stack + end; + list = { + elem = { + key = 'starsoul:canister_elem'; + accept = 'powder'; + sz = c.slots; + }; + }; + }; + }; + }) +end) ADDED mods/starsoul/fab.lua Index: mods/starsoul/fab.lua ================================================================== --- mods/starsoul/fab.lua +++ mods/starsoul/fab.lua @@ -0,0 +1,215 @@ +-- [ʞ] fab.lua +-- ~ lexi hale +-- 🄯 EUPL1.2 +-- ? fabrication spec class +-- a type.fab supports two operators: +-- +-- + used for compounding recipes. that is, +-- a+b = compose a new spec from the spec parts a and b. +-- this is used e.g. for creating tier-based +-- fabspecs. +-- +-- * used for determining quantities. that is, +-- f*x = spec to make x instances of f +-- +-- new fab fields must be defined in starsoul.type.fab.opClass. +-- this maps a name to fn(a,b,n) -> quant, where a is the first +-- argument, b is a compounding amount, and n is a quantity of +-- items to produce. fields that are unnamed will be underwritten + +local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end +local function fFac (a,b,n) + if a == nil and b == nil then return nil end + local f if a == nil or b == nil then + f = a or b + else + f = (a or 1)*(b or 1) + end + return f*n +end +local function fReq (a,b,n) return a or b end +local function fFlag (a,b,n) return a and b end +local function fSize (a,b,n) return math.max(a,b) end +local opClass = { + -- fabrication eligibility will be determined by which kinds + -- of input a particular fabricator can introduce. e.g. a + -- printer with a but no cache can only print items whose + -- recipe only names elements as ingredients + + -- ingredients + element = fQuant; -- (g) + gas = fQuant; -- () + liquid = fQuant; -- (l) + crystal = fQuant; -- (g) + item = fQuant; -- n + metal = fQuant; -- (g) + metalIngot = fQuant; -- (g) + -- factors + cost = fFac; -- units vary + time = fFac; -- (s) + -- print: base printing time + size = fSize; + -- printBay: size of the printer bay necessary to produce the item + req = fReq; + flag = fFlag; -- means that can be used to produce the item & misc flags + -- print: allow production with a printer + -- smelt: allow production with a smelter + -- all else defaults to underwrite +} + +local F = string.format +local strClass = { + element = function(x, n) + local el = starsoul.world.material.element[x] + return lib.math.si('g', n) .. ' ' .. (el.sym or el.name) + end; + metal = function(x, n) + local met = starsoul.world.material.metal[x] + return lib.math.si('g', n) .. ' ' .. met.name + end; + liquid = function(x, n) + local liq = starsoul.world.material.liquid[x] + return lib.math.si('L', n) .. ' ' .. liq.name + end; + gas = function(x, n) + local gas = starsoul.world.material.gas[x] + return lib.math.si('g', n) .. ' ' .. gas.name + end; + item = function(x, n) + local i = minetest.registered_items[x] + return tostring(n) .. 'x ' .. i.short_description + end; +} + +local order = { + 'element', 'metal', 'liquid', 'gas', 'item' +} + +local lib = starsoul.mod.lib +local fab fab = lib.class { + __name = 'starsoul:fab'; + + opClass = opClass; + strClass = strClass; + order = order; + construct = function(q) return q end; + __index = { + elementalize = function(self) + local e = fab {element = self.element or {}} + for _, kind in pairs {'metal', 'gas', 'liquid'} do + for m,mass in pairs(self[kind] or {}) do + local mc = starsoul.world.material[kind][m].composition + e = e + mc:elementalize()*mass + end + end + return e + end; + + elementSeq = function(self) + local el = {} + local em = self.element + local s = 0 + local eldb = starsoul.world.material.element.db + for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end + table.sort(el, function(a,b) + return eldb[a].n > eldb[b].n + end) + return el, em, s + end; + + formula = function(self) + print('make formula', dump(self)) + local ts,f=0 + if self.element then + f = {} + local el, em, s = self:elementSeq() + local eldb = starsoul.world.material.element.db + for i, e in ipairs(el) do + local sym, n = eldb[e].sym, em[e] + if n > 0 then + table.insert(f, string.format("%s%s", + sym, n>1 and lib.str.nIdx(n) or '')) + end + end + f = table.concat(f) + ts = ts + s + end + + local sub = {} + for _, w in pairs {'metal', 'gas', 'liquid'} do + if self[w] then + local mdb = starsoul.world.material[w].db + for k, amt in pairs(self[w]) do + local mf, s = mdb[k].composition:formula() + if amt > 0 then table.insert(sub, { + f = string.format("(%s)%s",mf, + lib.str.nIdx(amt)); + s = s; + }) end + ts = ts + s*amt + end + end + end + table.sort(sub, function(a,b) return a.s > b.s end) + local fml = {} + for i, v in ipairs(sub) do fml[i] = v.f end + if f then table.insert(fml, f) end + fml = table.concat(fml, ' + ') + + return fml, ts + end; + }; + + __tostring = function(self) + local t = {} + for i,o in ipairs(order) do + if self[o] then + for mat,amt in pairs(self[o]) do + if amt > 0 then + table.insert(t, strClass[o](mat, amt)) + end + end + end + end + return table.concat(t, ", ") + end; + + + __add = function(a,b) + local new = fab {} + for cat, vals in pairs(a) do + new[cat] = lib.tbl.copy(vals) + end + for cat, vals in pairs(b) do + if not new[cat] then + new[cat] = lib.tbl.copy(vals) + else + local f = opClass[cat] + for k,v in pairs(vals) do + local n = f(new[cat][k], v, 1) + new[cat][k] = n > 0 and n or nil + end + end + end + return new + end; + + __mul = function(x,n) + local new = fab {} + for cat, vals in pairs(x) do + new[cat] = {} + local f = opClass[cat] + for k,v in pairs(vals) do + local num = f(v,nil,n) + new[cat][k] = num > 0 and num or nil + end + end + return new + end; + + __div = function(x,n) + return x * (1/n) + end; +} + +starsoul.type.fab = fab ADDED mods/starsoul/fx/nano.lua Index: mods/starsoul/fx/nano.lua ================================================================== --- mods/starsoul/fx/nano.lua +++ mods/starsoul/fx/nano.lua @@ -0,0 +1,180 @@ +local lib = starsoul.mod.lib +local E = starsoul.effect +local N = {} +starsoul.fx.nano = N +local nanopool= { + { + name = 'starsoul-fx-nano-spark-small.png'; + scale_tween = {0,.5, style = 'pulse', rep = 3}; + }; + { + name = 'starsoul-fx-nano-spark-small.png'; + scale_tween = {0,1, style = 'pulse', rep = 2}; + }; + { + name = 'starsoul-fx-nano-spark-big.png'; + scale_tween = {0,1, style = 'pulse'}; + }; +} + +function N.heal(user, targets, amt, dur) + local amthealed = {} + local f = E.cast { + caster = user.entity; + subjects = targets; + duration = dur; + intervals = { + { + after = 0; + period = 4; + fn = function(c) + for i,v in pairs(c.effect.subjects) do + local u = starsoul.activeUsers[v.player:get_player_name()] + if u then + local heal = math.max(amt/4, 1) + amthealed[u] = amthealed[u] or 0 + if amthealed[u] < amt then + amthealed[u] = amthealed[u] + heal + u:statDelta('health', heal) + end + end + end + end; + } + } + } + + local casterIsTarget = false + for _, sub in pairs(f.subjects) do + if sub.player == user.entity then + casterIsTarget = true + end + f.visual(sub, { + amount = 50; + time = dur; + glow = 14; + jitter = 0.01; + attached = user.entity; + vel = { min = -0.1, max = 0.1; }; + pos = { + min = vector.new(0,0.2,0); + max = vector.new(0,1.2,0); + }; + radius = { min = 0.2; max = 0.6; bias = -1; }; + exptime = {min=0.5,max=2}; + attract = { + kind = 'line'; + strength = {min = 0.5, max = 2}; + origin = 0; + direction = vector.new(0,1,0); + origin_attached = sub.player; + direction_attached = sub.player; + }; + + texpool = nanopool; + }) + end + if not casterIsTarget then + -- f.visual_caster { } + end + f.play(0.3, { + where = 'subjects'; + sound = 'starsoul-nano-heal'; + ephemeral = true; + spec = {gain = 0.3}; + }) + + return f +end + +function N.shred(user, pos, prop, time, node) + local f = E.cast { + caster = user.entity; + subjects = {}; + duration = time; + } + local sp,sv = user:lookupSpecies() + local eh = sv.eyeHeight or sp.eyeHeight + f.visual_caster { + amount = 200 * time; + pos = vector.new(0.12,eh - 0.1,0); + radius = 0.2; + time = time - (time/3); + glow = 14; + jitter = 0.1; + size = {min = 0.2, max = 0.5}; + exptime = {min=0.5,max=1}; + vel_tween = { + 0; + { min = -0.4, max = 0.4; }; + style = 'pulse', rep = time * 2; + }; + attract = { + kind = 'point'; + origin = pos; + radius = 0.5; + strength = {min=.3,max=2}; + }; + texpool = nanopool; + }; + f.queue(0.05, function(s, timepast, timeleft) + f.visual(nil, { + amount = timeleft * 40; + time = timeleft; + pos = pos; + size_tween = { + 0, {min = 0.5, max = 2}; + }; + vel = { + min = vector.new(-1.2,0.5,-1.2); + max = vector.new(1.2,3.5,1.2); + }; + acc = vector.new(0,-starsoul.world.planet.gravity,0); + node = node; + }) + end); + f.queue(0.9, function(s, timepast, timeleft) + f.visual(nil, { + amount = 200; + time = timeleft; + pos = pos; + size = {min = 0.1, max = 0.3}; + vel = { + min = vector.new(-2,0.5,-2); + max = vector.new(2,4,2); + }; + acc = vector.new(0,-starsoul.world.planet.gravity,0); + node = node; + }) + end); + f.queue(0.3, function(s, timepast, timeleft) + local function v(fn) + local def = { + amount = timeleft * 100; + pos = pos; + time = timeleft; + radius = 0.5; + jitter = {min = 0.0, max = 0.2}; + size = {min = 0.2, max = 0.5}; + exptime = {min = 0.5, max = 1}; + attract = { + kind = 'point'; + strength = {min=0.3, max = 1}; + origin = vector.new(0,eh-0.1,0); + radius = 0.5; + origin_attached = user.entity; + }; + } + fn(def) + f.visual(nil, def) + end + v(function(t) t.texpool = nanopool t.glow = 14 end) + v(function(t) + t.node = node + t.amount = timeleft * 20 + t.size = {min = 0.1, max = 0.3}; + end) + end) + return f + +end ADDED mods/starsoul/init.lua Index: mods/starsoul/init.lua ================================================================== --- mods/starsoul/init.lua +++ mods/starsoul/init.lua @@ -0,0 +1,411 @@ +-- [ʞ] starsoul/init.lua +-- ~ lexi hale +-- ? basic setup, game rules, terrain +-- © EUPL v1.2 + +local T = minetest.get_translator 'starsoul' + +-- TODO enforce latest engine version + +local mod = { + -- subordinate mods register here + lib = vtlib; + -- vtlib should be accessed as starsoul.mod.lib by starsoul modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency +} +local lib = mod.lib + + +starsoul = { + ident = minetest.get_current_modname(); + mod = mod; + translator = T; + + constant = { + light = { --minetest units + dim = 3; + lamp = 7; + bright = 10; + brightest = 14; -- only sun and growlights + }; + heat = { -- celsius + freezing = 0; + safe = 4; + overheat = 32; + boiling = 100; + }; + rad = { + }; + }; + + activeUsers = { + -- map of username -> user object + }; + activeUI = { + -- map of username -> UI context + }; + liveUI = { + -- cached subset of activeUI containing those UIs needing live updates + }; + + interface = lib.registry.mk 'starsoul:interface'; + item = { + }; + + region = { + radiator = { + store = AreaStore(); + emitters = {} + }; + }; + + -- standardized effects + fx = {}; + + type = {}; + world = { + defaultScenario = 'starsoul_scenario:imperialExpat'; + seedbank = lib.math.seedbank(minetest.get_mapgen_setting 'seed'); + mineral = lib.registry.mk 'starsoul:mineral'; + material = { -- raw materials + element = lib.registry.mk 'starsoul:element'; + -- elements are automatically sorted into the following categories + -- if they match. however, it's possible to have a metal/gas/liquid + -- that *isn't* a pure element, so these need separate registries + -- for alloys and mixtures like steel and water + metal = lib.registry.mk 'starsoul:metal'; + gas = lib.registry.mk 'starsoul:gas'; + liquid = lib.registry.mk 'starsoul:liquid'; + }; + ecology = { + plants = lib.registry.mk 'starsoul:plants'; + trees = lib.registry.mk 'starsoul:trees'; + biomes = lib.registry.mk 'starsoul:biome'; + }; + climate = {}; + scenario = {}; + planet = { + gravity = 7.44; + orbit = 189; -- 1 year is 189 days + revolve = 20; -- 1 day is 20 irl minutes + }; + fact = lib.registry.mk 'starsoul:fact'; + time = { + calendar = { + empire = { + name = 'Imperial Regnal Calendar'; + year = function(t, long) + local reigns = { + -- if anyone actually makes it to his Honor & Glory Unfailing Persigan I i will be + -- exceptionally flattered + {4, 'Emperor', 'Atavarka', 'the Bold'}; -- died at war + {9, 'Emperor', 'Vatikserka', 'the Unconquered'}; -- died at war + {22, 'Emperor', 'Rusifend', 'the Wise'}; -- poisoned at diplomacy + {61, 'Empress', 'Tafseshendi', 'the Great'}; -- died of an 'insurrection of the innards' after a celebrated reign + {291, 'Emperor', 'Treptebaska', 'the Unwise'}; -- murdered by his wife in short order + {292, 'Empress', 'Vilintalti', 'the Impious'}; -- removed by the praetorian elite + {298, 'Emperor', 'Radavan', 'the Reckless'}; -- died at war + {316, 'Emperor', 'Suldibrand', 'the Forsaken of Men'}; -- fucked around. found out. + {320, 'Emperor', 'Persigan', 'the Deathless'}; + } + local year, r = math.floor(t / 414) + for i=1, #reigns do if reigns[i+1][1] < year then r = reigns[i+1] end end + local reignBegin, title, name, epithet = lib.tbl.unpack(r) + local ry = 1 + (year - reignBegin) + return long and string.format('Year %s of the Reign of HH&GU %s %s %s', + ry, title, name, epithet) or string.format('Y. %s %s', name, ry) + end; + time = function(t, long) + local bellsInDay, candleSpansInBell = 5, 7 + local bell = bellsInDay*t + local cspan = (bellsInDay*candleSpansInBell*t) % candleSpansInBell + return string.format(long and 'Bell %s, Candlespan %s' or '%sb %sc', math.floor(bell), math.floor(cspan)) + end; + }; + commune = { + name = 'People\'s Calendar'; + date = function(t, long) + local year = math.floor(t / 256) + 314 + return string.format(long and 'Foundation %s' or 'F:%s', year) + end; + time = function(t, long) + local hoursInDay, minutesInHour = 16, 16 + local hour = hoursInDay*t + local min = (hoursInDay*minutesInHour*t) % minutesInHour + + local dawn = 0.24*hoursInDay + local noon = 0.5*hoursInDay + local dusk = 0.76*hoursInDay + local midnight = 1.0*hoursInDay + + local tl, str + if hour < dawn then + tl = dawn - hour + str = long and 'dawn' or 'D' + elseif hour < noon then + tl = noon - hour + str = long and 'noon' or 'N' + elseif hour < dusk then + tl = dusk - hour + str = long and 'dusk' or 'd' + elseif hour < midnight then + tl = midnight - hour + str = long and 'midnight' or 'M' + end + return long + and string.format('%s hours, %s minutes to %s', + math.floor(tl), math.floor(minutesInHour - min), str) + or string.format('%s.%sH.%sM', str, math.floor(tl), + math.floor(minutesInHour - min)) + end; + }; + }; + }; + }; + + jobs = {}; +} + +starsoul.cfgDir = minetest.get_worldpath() .. '/' .. starsoul.ident + +local logger = function(module) + local function argjoin(arg, nxt, ...) + if arg and not nxt then return tostring(arg) end + if not arg then return "(nil)" end + return tostring(arg) .. ' ' .. argjoin(nxt, ...) + end + local lg = {} + local setup = function(fn, lvl) + lvl = lvl or fn + local function emit(...) + local call = (fn == 'fatal') and error + or function(str) minetest.log(lvl, str) end + if module + then call(string.format('[%s :: %s] %s',starsoul.ident,module,argjoin(...))) + else call(string.format('[%s] %s',starsoul.ident,argjoin(...))) + end + end + lg[fn ] = function(...) emit(...) end + lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn + end + setup('info') + setup('warn','warning') + setup('err','error') + setup('act','action') + setup('fatal') + return lg +end + +starsoul.logger = logger + +local log = logger() + +function starsoul.evaluate(name, ...) + local path = minetest.get_modpath(minetest.get_current_modname()) + local filename = string.format('%s/%s', path, name) + log.info('loading', filename) + local chunk, err = loadfile(filename, filename) + if not chunk then error(err) end + return chunk(...) +end + +function starsoul.include(name, ...) -- semantic variant used for loading modules + return starsoul.evaluate(name..'.lua', ...) +end + +minetest.register_lbm { + label = 'build radiator index'; + name = 'starsoul:loadradiatorboxes'; + nodenames = {'group:radiator'}; + run_at_every_load = true; + action = function(pos, node, dt) + local R = starsoul.region + local phash = minetest.hash_node_position(pos) + if R.radiator.sources[phash] then return end -- already loaded + + local def = minetest.registered_nodes[node.name] + local cl = def._starsoul.radiator + local min,max = cl.maxEffectArea(pos) + local id = R.radiator.store:insert_area(min,max, minetest.pos_to_string(pos)) + R.radiator.sources[phash] = id + end; + -- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb +} + +function starsoul.startJob(id, interval, job) + local lastRun + local function start() + starsoul.jobs[id] = minetest.after(interval, function() + local t = minetest.get_gametime() + local d = lastRun and t - lastRun or nil + lastRun = t + local continue = job(d, interval) + if continue == true or continue == nil then + start() + elseif continue ~= false then + interval = continue + start() + end + end) + end + start() +end + +starsoul.include 'stats' +starsoul.include 'world' +starsoul.include 'fab' +starsoul.include 'tiers' +starsoul.include 'species' + +starsoul.include 'store' + +starsoul.include 'ui' +starsoul.include 'item' +starsoul.include 'container' +starsoul.include 'user' +starsoul.include 'effect' + +starsoul.include 'fx/nano' + +starsoul.include 'element' + +starsoul.include 'terrain' +starsoul.include 'interfaces' +starsoul.include 'suit' + +minetest.settings:set('movement_gravity', starsoul.world.planet.gravity) -- ??? seriously??? + +--------------- +-- callbacks -- +--------------- +-- here we connect our types up to the minetest API + +local function userCB(fn) + return function(luser, ...) + local name = luser:get_player_name() + local user = starsoul.activeUsers[name] + return fn(user, ...) + end +end + +minetest.register_on_joinplayer(function(luser, lastLogin) + -- TODO check that necessary CSMs are installed + local user = starsoul.type.user(luser) + + if lastLogin == nil then + user:onSignup() + end + user:onJoin() + + starsoul.activeUsers[user.name] = user +end) + +minetest.register_on_leaveplayer(function(luser) + starsoul.activeUsers[luser:get_player_name()]:onPart() +end) + +minetest.register_on_player_receive_fields(function(luser, formid, fields) + local name = luser:get_player_name() + local user = starsoul.activeUsers[name] + if not user then return false end + if formid == '' then -- main menu + return starsoul.ui.userMenuDispatch(user,fields) + end + local ui = starsoul.interface.db[formid] + local state = starsoul.activeUI[name] or {} + if formid == '__builtin:help_cmds' + or formid == '__builtin:help_privs' + then return false end + assert(state.form == formid) -- sanity check + user:onRespond(ui, state, fields) + if fields.quit then + starsoul.activeUI[name] = nil + end + return true +end) + +minetest.register_on_respawnplayer(userCB(function(user) + return user:onRespawn() +end)) + +minetest.register_on_dieplayer(userCB(function(user, reason) + return user:onDie(reason) +end)) + +minetest.register_on_punchnode(function(pos,node,puncher,point) + local user = starsoul.activeUsers[puncher:get_player_name()] + local oldTgt = user.action.tgt + user.action.tgt = point + if bit.band(user.action.bits, 0x80)==0 then + user.action.bits = bit.bor(user.action.bits, 0x80) + --user:trigger('primary', {state = 'init'}) + else + user:trigger('retarget', {oldTgt = oldTgt}) + end +end) + +local function pointChanged(a,b) + return a.type ~= b.type + or a.type == 'node' and vector.new(a.under) ~= vector.new(b.under) + or a.type == 'object' and a.ref ~= b.ref +end +local function triggerPower(_, luser, point) + local user = starsoul.activeUsers[luser:get_player_name()] + local oldTgt = user.action.tgt + user.action.tgt = point + if bit.band(user.action.bits, 0x100)==0 then + user.action.bits = bit.bor(user.action.bits, 0x100) + --return user:trigger('secondary', {state = 'prog', delta = 0}) + elseif pointChanged(oldTgt, point) then + user:trigger('retarget', {oldTgt = oldTgt}) + end +end +-- sigh +core.noneitemdef_default.on_place = function(...) + if not triggerPower(...) then + minetest.item_place(...) + end +end +core.noneitemdef_default.on_use = function(...) triggerPower(...) end +core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end + +minetest.register_on_player_inventory_action(function(luser, act, inv, p) + local name = luser:get_player_name() + local user = starsoul.activeUsers[name] + -- allow UIs to update on UI changes + local state = starsoul.activeUI[name] + if state then + local ui = starsoul.interface.db[state.form] + ui:cb('onMoveItem', user, act, inv, p) + end +end) + +minetest.register_on_player_hpchange(function(luser, delta, cause) + local user = starsoul.activeUsers[luser:get_player_name()] + if cause.type == 'fall' then + delta = user:damageModifier('bluntForceTrauma', (delta * 50)) + -- justification: a short fall can do around + -- five points of damage, which is nearly 50% + -- of the default hp_max. since we crank up + -- hp by a factor of 50~40, damage should be + -- cranked by similarly + end + return delta +end, true) + +function minetest.handle_node_drops(pos, drops, digger) + local function jitter(pos) + local function r(x) return x+math.random(-0.2, 0.2) end + return vector.new( + r(pos.x), + r(pos.y), + r(pos.z) + ) + end + for i, it in ipairs(drops) do + minetest.add_item(jitter(pos), it) + end +end + + +-- TODO timer iterates live UI + ADDED mods/starsoul/interfaces.lua Index: mods/starsoul/interfaces.lua ================================================================== --- mods/starsoul/interfaces.lua +++ mods/starsoul/interfaces.lua @@ -0,0 +1,441 @@ +local lib = starsoul.mod.lib + +function starsoul.ui.setupForUser(user) + local function cmode(mode) + if user.actMode == mode then return {hue = 150, sat = 0, lum = .3} end + end + user.entity:set_inventory_formspec(starsoul.ui.build { + kind = 'vert', mode = 'sw'; + padding = .5, spacing = 0.1; + {kind = 'hztl'; + {kind = 'contact', w=1.5,h=1.5, id = 'mode_nano', + img='starsoul-ui-icon-nano.png', close=true, color = cmode'nano'}; + {kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon', + img='starsoul-ui-icon-weapon.png', close=true, color = cmode'weapon'}; + {kind = 'contact', w=1.5,h=1.5, id = 'mode_psi', + img='starsoul-ui-icon-psi.png', close=true, color = cmode'psi'}; + }; + {kind = 'hztl'; + {kind = 'contact', w=1.5,h=1.5, id = 'open_elements', + img='starsoul-ui-icon-element.png'}; + {kind = 'contact', w=1.5,h=1.5, id = 'open_suit', + img='starsoul-item-suit.png^[hsl:200:-.7:0'}; + {kind = 'contact', w=1.5,h=1.5, id = 'open_psi', + img='starsoul-ui-icon-psi-cfg.png'}; + {kind = 'contact', w=1.5,h=1.5, id = 'open_body', + img='starsoul-ui-icon-self.png'}; + }; + {kind = 'list'; + target = 'current_player', inv = 'main'; + w = 6, h = 1, spacing = 0.1; + }; + }) +end + +function starsoul.ui.userMenuDispatch(user, fields) + local function setSuitMode(mode) + if user.actMode == mode then + user:actModeSet 'off' + else + user:actModeSet(mode) + end + end + + local modes = { nano = true, psi = false, weapon = true } + for e,s in pairs(modes) do + if fields['mode_' .. e] then + if s and (user:naked() or user:getSuit():powerState() == 'off') then + user:suitSound 'starsoul-error' + else + setSuitMode(e) + end + return true + end + end + + if fields.open_elements then + user:openUI('starsoul:user-menu', 'compiler') + return true + elseif fields.open_psi then + user:openUI('starsoul:user-menu', 'psi') + return true + elseif fields.open_suit then + if not user:naked() then + user:openUI('starsoul:user-menu', 'suit') + end + return true + elseif fields.open_body then + user:openUI('starsoul:user-menu', 'body') + end + return false +end + +local function listWrap(n, max) + local h = math.ceil(n / max) + local w = math.min(max, n) + return w, h +end + +local function wrapMenu(w, h, rh, max, l) + local root = {kind = 'vert', w=w, h=h} + local bar + local function flush() + if bar and bar[1] then table.insert(root, bar) end + bar = {kind = 'hztl'} + end + flush() + + for _, i in ipairs(l) do + local bw = w/max + if i.cfg then w = w - rh end + + table.insert(bar, { + kind = 'button', close = i.close; + color = i.color; + fg = i.fg; + label = i.label; + icon = i.img; + id = i.id; + w = bw, h = rh; + }) + if i.cfg then + table.insert(bar, { + kind = 'button'; + color = i.color; + fg = i.fg; + label = "CFG"; + icon = i.img; + id = i.id .. '_cfg'; + w = rh, h = rh; + }) + end + + if bar[max] then flush() end + end + flush() + + return root +end + +local function abilityMenu(a) + -- select primary/secondary abilities or activate ritual abilities + local p = {kind = 'vert'} + for _, o in ipairs(a.order) do + local m = a.menu[o] + table.insert(p, {kind='label', text=m.label, w=a.w, h = .5}) + table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts)) + end + return p +end + +local function pptrMatch(a,b) + if a == nil or b == nil then return false end + return a.chipID == b.chipID and a.pgmIndex == b.pgmIndex +end + +starsoul.interface.install(starsoul.type.ui { + id = 'starsoul:user-menu'; + pages = { + compiler = { + setupState = function(state, user) + -- nanotech/suit software menu + local chips = user.entity:get_inventory():get_list 'starsoul_suit_chips' -- FIXME need better subinv api + local sw = starsoul.mod.electronics.chip.usableSoftware(chips) + state.suitSW = {} + local dedup = {} + for i, r in ipairs(sw) do if + r.sw.kind == 'suitPower' + then + if not dedup[r.sw] then + dedup[r.sw] = true + table.insert(state.suitSW, r) + end + end end + end; + handle = function(state, user, act) + if user:getSuit():powerState() == 'off' then return false end + local pgm, cfg + for k in next, act do + local id, mode = k:match('^suit_pgm_([0-9]+)_(.*)$') + if id then + id = tonumber(id) + if state.suitSW[id] then + pgm = state.suitSW[id] + cfg = mode == '_cfg' + break + end + end + end + if not pgm then return false end -- HAX + + -- kind=active programs must be assigned to a command slot + -- kind=direct programs must open their UI + -- kind=passive programs must toggle on and off + if pgm.sw.powerKind == 'active' then + if cfg then + user:openUI(pgm.sw.ui, 'index', { + context = 'suit'; + program = pgm; + }) + return false + end + local ptr = {chipID = starsoul.mod.electronics.chip.read(pgm.chip).uuid, pgmIndex = pgm.fd.inode} + local pnan = user.power.nano + if pnan.primary == nil then + pnan.primary = ptr + elseif pptrMatch(ptr, pnan.primary) then + pnan.primary = nil + elseif pptrMatch(ptr, pnan.secondary) then + pnan.secondary = nil + else + pnan.secondary = ptr + end + user:suitSound 'starsoul-configure' + elseif pgm.sw.powerKind == 'direct' then + local ctx = { + context = 'suit'; + program = pgm; + } + if pgm.sw.ui then + user:openUI(pgm.sw.ui, 'index', ctx) + return false + else + pgm.sw.run(user, ctx) + end + elseif pgm.sw.powerKind == 'passive' then + if cfg then + user:openUI(pgm.sw.ui, 'index', { + context = 'suit'; + program = pgm; + }) + return false + end + + local addDisableRec = true + for i, e in ipairs(pgm.file.body.conf) do + if e.key == 'disable' and e.value == 'yes' then + addDisableRec = false + table.remove(pgm.file.body.conf, i) + break + elseif e.key == 'disable' and e.value == 'no' then + e.value = 'yes' + addDisableRec = false + break + end + end + if addDisableRec then + table.insert(pgm.file.body.conf, {key='disable',value='yes'}) + end + -- update the chip *wince* + pgm.fd:write(pgm.file) + user.entity:get_inventory():set_stack('starsoul_suit_chips', + pgm.chipSlot, pgm.chip) + user:reconfigureSuit() + user:suitSound('starsoul-configure') + + end + return true, true + end; + render = function(state, user) + local suit = user:getSuit() + local swm + if user:getSuit():powerState() ~= 'off' then + swm = { + w = 8, h = 3; + order = {'active','ritual','pasv'}; + menu = { + active = { + label = 'Nanoware'; + opts = {}; + }; + ritual = { + label = 'Programs'; + opts = {}; + }; + pasv = { + label = 'Passive'; + opts = {}; + }; + }; + } + for id, r in pairs(state.suitSW) do + local color = {hue=300,sat=0,lum=0} + local fg = nil + local close = nil + local tbl, cfg if r.sw.powerKind == 'active' then + tbl = swm.menu.active.opts + if r.sw.ui then cfg = true end + local pnan = user.power.nano + if pnan then + local ptr = {chipID = starsoul.mod.electronics.chip.read(r.chip).uuid, pgmIndex = r.fd.inode} + if pptrMatch(ptr, pnan.primary) then + color.lum = 1 + elseif pptrMatch(ptr, pnan.secondary) then + color.lum = 0.8 + end + end + elseif r.sw.powerKind == 'direct' then + tbl = swm.menu.ritual.opts + if not r.sw.ui then + close = true + end + elseif r.sw.powerKind == 'passive' then + tbl = swm.menu.pasv.opts + if r.sw.ui then cfg = true end + for i, e in ipairs(r.file.body.conf) do + if e.key == 'disable' and e.value == 'yes' then + color.lum = -.2 + fg = lib.color {hue=color.hue,sat=0.7,lum=0.7} + break + end + end + end + if tbl then table.insert(tbl, { + color = color, fg = fg; + label = r.sw.label or r.sw.name; + id = string.format('suit_pgm_%s_', id); + cfg = cfg, close = close; + }) end + end + end + local menu = { kind = 'vert', mode = 'sw', padding = 0.5 } + if swm then table.insert(menu, abilityMenu(swm)) end + + local inv = user.entity:get_inventory() + local cans = inv:get_list 'starsoul_suit_canisters' + if cans and next(cans) then for i, st in ipairs(cans) do + local id = string.format('starsoul_canister_%u_elem', i) + local esz = inv:get_size(id) + if esz > 0 then + local eltW, eltH = listWrap(esz, 5) + table.insert(menu, {kind = 'hztl', + {kind = 'img', desc='Elements', img = 'starsoul-ui-icon-element.png', w=1,h=1}; + {kind = 'list', target = 'current_player', inv = id, + listContent = 'element', w = eltW, h = eltH, spacing = 0.1}; + }) + end + end end + + if #menu == 0 then + table.insert(menu, { + kind = 'img'; + img = 'starsoul-ui-alert.png'; + w=2, h=2; + }) + menu.padding = 1; + end + return starsoul.ui.build(menu) + end; + }; + compilerListRecipes = { + }; + psi = { + render = function(state, user) + return starsoul.ui.build { + kind = 'vert', mode = 'sw'; + padding = 0.5; + } + end; + }; + body = { + render = function(state, user) + local barh = .75 + local tb = { + kind = 'vert', mode = 'sw'; + padding = 0.5, + {kind = 'hztl', padding = 0.25; + {kind = 'label', text = 'Name', w = 2, h = barh}; + {kind = 'label', text = user.persona.name, w = 4, h = barh}}; + } + local statBars = {'hunger', 'thirst', 'fatigue', 'morale'} + for idx, id in ipairs(statBars) do + local s = starsoul.world.stats[id] + local amt, sv = user:effectiveStat(id) + local min, max = starsoul.world.species.statRange(user.persona.species, user.persona.speciesVariant, id) + local st = string.format('%s / %s', s.desc(amt, true), s.desc(max)) + table.insert(tb, {kind = 'hztl', padding = 0.25; + {kind = 'label', w=2, h=barh, text = s.name}; + {kind = 'hbar', w=4, h=barh, fac = sv, text = st, color=s.color}; + }) + end + local abilities = { + {id = 'abl_sprint', label = 'Sprint', img = 'starsoul-ui-icon-ability-sprint.png'}; + } + table.insert(tb, wrapMenu(6.25,4, 1,2, abilities)) + return starsoul.ui.build(tb) + end; + }; + suit = { + render = function(state, user) + local suit = user:getSuit() + local suitDef = suit:def() + local chipW, chipH = listWrap(suitDef.slots.chips, 5) + local batW, batH = listWrap(suitDef.slots.batteries, 5) + local canW, canH = listWrap(suitDef.slots.canisters, 5) + local suitMode = suit:powerState() + local function modeColor(mode) + if mode == suitMode then return {hue = 180, sat = 0, lum = .5} end + end + return starsoul.ui.build { + kind = 'vert', mode = 'sw'; + padding = 0.5, spacing = 0.1; + {kind = 'hztl', + {kind = 'img', desc='Batteries', img = 'starsoul-item-battery.png', w=1,h=1}; + {kind = 'list', target = 'current_player', inv = 'starsoul_suit_bat', + listContent = 'power', w = batW, h = batH, spacing = 0.1}; + }; + {kind = 'hztl', + {kind = 'img', desc='Chips', img = 'starsoul-item-chip.png', w=1,h=1}; + {kind = 'list', target = 'current_player', inv = 'starsoul_suit_chips', + listContent = 'chip', w = chipW, h = chipH, spacing = 0.1}; + }; + {kind = 'hztl', + {kind = 'img', desc='Canisters', img = 'starsoul-item-element-canister.png', w=1,h=1}; + {kind = 'list', target = 'current_player', inv = 'starsoul_suit_canisters', + listContent = nil, w = canW, h = canH, spacing = 0.1}; + }; + {kind = 'hztl'; + {kind = 'img', w=1,h=1, item = suit.item:get_name(), + desc = suit.item:get_definition().short_description}; + {kind = 'button', w=1.5,h=1, id = 'powerMode_off', label = 'Off'; + color=modeColor'off'}; + {kind = 'button', w=2.5,h=1, id = 'powerMode_save', label = 'Power Save'; + color=modeColor'powerSave'}; + {kind = 'button', w=1.5,h=1, id = 'powerMode_on', label = 'On'; + color=modeColor'on'}; + }; + {kind = 'list', target = 'current_player', inv = 'main', w = 6, h = 1, spacing = 0.1}; + } + end; + handle = function(state, user, q) + local suitMode + if q.powerMode_off then suitMode = 'off' + elseif q.powerMode_save then suitMode = 'powerSave' + elseif q.powerMode_on then suitMode = 'on' end + if suitMode then + user:suitPowerStateSet(suitMode) + return true + end + end; + }; + }; +}) + +starsoul.interface.install(starsoul.type.ui { + id = 'starsoul:compile-matter-component'; + pages = { + index = { + setupState = function(state, user, ctx) + if ctx.context == 'suit' then + end + state.pgm = ctx.program + end; + render = function(state, user) + return starsoul.ui.build { + kind = 'vert', padding = 0.5; w = 5, h = 5, mode = 'sw'; + {kind = 'label', w = 4, h = 1, text = 'hello'}; + } + end; + }; + }; +}) ADDED mods/starsoul/item.lua Index: mods/starsoul/item.lua ================================================================== --- mods/starsoul/item.lua +++ mods/starsoul/item.lua @@ -0,0 +1,17 @@ +local lib = starsoul.mod.lib +local I = starsoul.item + +function I.mk(item, context) + local st = ItemStack(item) + local md = st:get_definition()._starsoul + local ctx = context or {} + if md and md.event then + md.event.create(st, ctx) + end + if context.how == 'print' then + if context.schematic and context.schematic.setup then + context.schematic.setup(st, ctx) + end + end + return st +end ADDED mods/starsoul/mod.conf Index: mods/starsoul/mod.conf ================================================================== --- mods/starsoul/mod.conf +++ mods/starsoul/mod.conf @@ -0,0 +1,4 @@ +name = starsoul +author = velartrill +description = world logic and UI +depends = vtlib ADDED mods/starsoul/species.lua Index: mods/starsoul/species.lua ================================================================== --- mods/starsoul/species.lua +++ mods/starsoul/species.lua @@ -0,0 +1,246 @@ +local lib = starsoul.mod.lib + +local paramTypes do local T,G = lib.marshal.t, lib.marshal.g + paramTypes = { + tone = G.struct { + hue = T.angle; + sat = T.clamp; + lum = T.clamp; + }; + str = T.str; + num = T.decimal; + } +end + +-- constants +local animationFrameRate = 60 + +local species = { + human = { + name = 'Human'; + desc = 'The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greatest Races by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.'; + scale = 1.0; + params = { + {'eyeColor', 'Eye Color', 'tone', {hue=327, sat=0, lum=0}}; + {'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}}; + {'skinTone', 'Skin Tone', 'tone', {hue= 0, sat=0, lum=0}}; + }; + tempRange = { + comfort = {18.3, 23.8}; -- needed for full stamina regen + survivable = {5, 33}; -- anything below/above will cause progressively more damage + }; + variants = { + female = { + name = 'Human Female'; + mesh = 'starsoul-body-female.x'; + eyeHeight = 1.4; + texture = function(t, adorn) + local skin = lib.image 'starsoul-body-skin.png' : shift(t.skinTone) + local eye = lib.image 'starsoul-body-eye.png' : shift(t.eyeColor) + local hair = lib.image 'starsoul-body-hair.png' : shift(t.hairColor) + + local invis = lib.image '[fill:1x1:0,0:#00000000' + local plate = adorn.suit and adorn.suit.plate or invis + local lining = adorn.suit and adorn.suit.lining or invis + + return {lining, plate, skin, skin, eye, hair} + end; + stats = { + psiRegen = 1.3; + psiPower = 1.2; + psi = 1.2; + hunger = .8; -- women have smaller stomachs + thirst = .8; + staminaRegen = 1.0; + morale = 0.8; -- you are not She-Bear Grylls + }; + traits = { + health = 400; + lungCapacity = .6; + irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya + sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma + metabolism = 1800; --Cal + painTolerance = 0.4; + }; + }; + male = { + name = 'Human Male'; + eyeHeight = 1.6; + stats = { + psiRegen = 1.0; + psiPower = 1.0; + psi = 1.0; + hunger = 1.0; + staminaRegen = .7; -- men are strong but have inferior endurance + }; + traits = { + health = 500; + painTolerance = 1.0; + lungCapacity = 1.0; + sturdiness = 0.3; + metabolism = 2200; --Cal + }; + }; + }; + traits = {}; + }; +} + +starsoul.world.species = { + index = species; + paramTypes = paramTypes; +} + +function starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant) + local sp = species[pSpecies] + local var = sp.variants[pVariant] + local vpd = var.defaults or {} + local tbl = {} + for _, p in pairs(sp.params) do + local name, desc, ty, dflt = lib.tbl.unpack(p) + tbl[name] = vpd[name] or dflt + end + return tbl +end + + +function starsoul.world.species.mkPersonaFor(pSpecies, pVariant) + return { + species = pSpecies; + speciesVariant = pVariant; + bodyParams = starsoul.world.species.paramsFromTable(pSpecies, + starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant) + ); + statDeltas = {}; + } +end + +local function spLookup(pSpecies, pVariant) + local sp = species[pSpecies] + local var = sp.variants[pVariant or next(sp.variants)] + return sp, var +end +starsoul.world.species.lookup = spLookup + +function starsoul.world.species.statRange(pSpecies, pVariant, pStat) + local sp,spv = spLookup(pSpecies, pVariant) + local min, max, base + if pStat == 'health' then + min,max = 0, spv.traits.health + elseif pStat == 'breath' then + min,max = 0, 65535 + else + local spfac = spv.stats[pStat] + local basis = starsoul.world.stats[pStat] + min,max = basis.min, basis.max + + if spfac then + min = min * spfac + max = max * spfac + end + + base = basis.base + if base == true then + base = max + elseif base == false then + base = min + end + + end + return min, max, base +end + +-- set the necessary properties and create a persona for a newspawned entity +function starsoul.world.species.birth(pSpecies, pVariant, entity, circumstances) + circumstances = circumstances or {} + local sp,var = spLookup(pSpecies, pVariant) + + local function pct(st, p) + local min, max = starsoul.world.species.statRange(pSpecies, pVariant, st) + local delta = max - min + return min + delta*p + end + local ps = starsoul.world.species.mkPersonaFor(pSpecies,pVariant) + local startingHP = pct('health', 1.0) + if circumstances.injured then startingHP = pct('health', circumstances.injured) end + if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end + ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite + + entity:set_properties{hp_max = var.traits.health or sp.traits.health} + entity:set_hp(startingHP, 'initial hp') + return ps +end + +function starsoul.world.species.paramsFromTable(pSpecies, tbl) + local lst = {} + local sp = species[pSpecies] + for i, par in pairs(sp.params) do + local name,desc,ty,dflt = lib.tbl.unpack(par) + if tbl[name] then + table.insert(lst, {id=name, value=paramTypes[ty].enc(tbl[name])}) + end + end + return lst +end +function starsoul.world.species.paramsToTable(pSpecies, lst) + local tymap = {} + local sp = species[pSpecies] + for i, par in pairs(sp.params) do + local name,desc,ty,dflt = lib.tbl.unpack(par) + tymap[name] = paramTypes[ty] + end + + local tbl = {} + for _, e in pairs(lst) do + tbl[e.id] = tymap[e.id].dec(e.value) + end + return tbl +end + +for speciesName, sp in pairs(species) do + for varName, var in pairs(sp.variants) do + if var.mesh then + var.animations = starsoul.evaluate(string.format('models/%s.nla', var.mesh)).skel.action + end + end +end + + +function starsoul.world.species.updateTextures(ent, persona, adornment) + local s,v = spLookup(persona.species, persona.speciesVariant) + local paramTable = starsoul.world.species.paramsToTable(persona.species, persona.bodyParams) + local texs = {} + for i, t in ipairs(v.texture(paramTable, adornment)) do + texs[i] = t:render() + end + ent:set_properties { textures = texs } +end + +function starsoul.world.species.setupEntity(ent, persona) + local s,v = spLookup(persona.species, persona.speciesVariant) + local _, maxHealth = starsoul.world.species.statRange(persona.species, persona.speciesVariant, 'health') + ent:set_properties { + visual = 'mesh'; + mesh = v.mesh; + stepheight = .51; + eye_height = v.eyeHeight; + collisionbox = { -- FIXME + -0.3, 0.0, -0.3; + 0.3, 1.5, 0.3; + }; + visual_size = vector.new(10,10,10) * s.scale; + + hp_max = maxHealth; + } + local function P(v) + if v then return {x=v[1],y=v[2]} end + return {x=0,y=0} + end + ent:set_local_animation( + P(v.animations.idle), + P(v.animations.run), + P(v.animations.work), + P(v.animations.runWork), + animationFrameRate) + +end ADDED mods/starsoul/stats.lua Index: mods/starsoul/stats.lua ================================================================== --- mods/starsoul/stats.lua +++ mods/starsoul/stats.lua @@ -0,0 +1,49 @@ +local lib = starsoul.mod.lib + +local function U(unit, prec, fixed) + if fixed then + return function(amt, excludeUnit) + if excludeUnit then return tostring(amt/prec) end + return string.format("%s %s", amt/prec, unit) + end + else + return function(amt, excludeUnit) + if excludeUnit then return tostring(amt/prec) end + return lib.math.si(unit, amt/prec) + end + end +end + +local function C(h, s, l) + return lib.color {hue = h, sat = s or 1, lum = l or .7} +end +starsoul.world.stats = { + psi = {min = 0, max = 500, base = 0, desc = U('ψ', 10), color = C(320), name = 'Numina'}; + -- numina is measured in daψ + warmth = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'Warmth'}; + -- warmth in measured in °C×10 + fatigue = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'Fatigue'}; + -- fatigue is measured in minutes one needs to sleep to cure it + stamina = {min = 0, max = 20 * 100, base = true, desc = U('m', 100), color = C(88), name = 'Stamina'}; + -- stamina is measured in how many 10th-nodes (== cm) one can sprint + hunger = {min = 0, max = 20000, base = 0, desc = U('Cal', 1), color = C(43,.5,.4), name = 'Hunger'}; + -- hunger is measured in calories one must consume to cure it + thirst = {min = 0, max = 1600, base = 0, desc = U('l', 100), color = C(217, .25,.4), name = 'Thirst'}; + -- thirst is measured in centiliters of H²O required to cure it + morale = {min = 0, max = 24 * 60 * 10, base = true, desc = U('hr', 60, true), color = C(0,0,.8), name = 'Morale'}; + -- morale is measured in minutes. e.g. at base rate morale degrades by + -- 60 points every hour. morale can last up to 10 days + irradiation = {min = 0, max = 20000, base = 0, desc = U('Gy', 1000), color = C(141,1,.5), name = 'Irradiation'}; + -- irrad is measured is milligreys + -- 1Gy counters natural healing + -- ~3Gy counters basic nanomedicine + -- 5Gy causes death within two weeks without nanomedicine + -- radiation speeds up psi regen + -- morale drain doubles with each 2Gy + illness = {min = 0, max = 1000, base = 0, desc = U('%', 10, true), color = C(71,.4,.25), name = 'Illness'}; + -- as illness increases, maximum stamina and health gain a corresponding limit + -- illness is increased by certain conditions, and decreases on its own as your + -- body heals when those conditions wear off. some drugs can lower accumulated illness + -- but illness-causing conditions require specific cures + -- illness also causes thirst and fatigue to increase proportionately +} ADDED mods/starsoul/store.lua Index: mods/starsoul/store.lua ================================================================== --- mods/starsoul/store.lua +++ mods/starsoul/store.lua @@ -0,0 +1,53 @@ +-- [ʞ] store.lua +-- ~ lexi hale +-- © EUPLv1.2 +-- ? defines serialization datatypes that don't belong to +-- any individual class + +local lib = starsoul.mod.lib +local T,G = lib.marshal.t, lib.marshal.g +starsoul.store = {} -- the serialization equivalent of .type + +------------- +-- persona -- +------------- ----------------------------------------------- +-- a Persona is a structure that defines the nature of -- +-- an (N)PC and how it interacts with the Starsoul-managed -- +-- portion of the game world -- things like name, species, -- +-- stat values, physical characteristics, and so forth -- + +local statStructFields = {} +for k,v in pairs(starsoul.world.stats) do + statStructFields[k] = v.srzType or ( + (v.base == true or v.base > 0) and T.s16 or T.u16 + ) +end + +starsoul.store.compilerJob = G.struct { + schematic = T.str; + progress = T.clamp; +} + +starsoul.store.persona = G.struct { + name = T.str; + species = T.str; + speciesVariant = T.str; + background = T.str; + bodyParams = G.array(8, G.struct {id = T.str, value = T.str}); --variant + + statDeltas = G.struct(statStructFields); + + facts = G.array(32, G.array(8, T.str)); + -- facts stores information the player has discovered and narrative choices + -- she has made. + -- parametric facts are encoded as horn clauses + -- non-parametric facts are encoded as {'fact-mod:fact-id'} +} + +starsoul.store.suitMeta = lib.marshal.metaStore { + batteries = {key = 'starsoul:suit_slots_bat', type = T.inventoryList}; + chips = {key = 'starsoul:suit_slots_chips', type = T.inventoryList}; + elements = {key = 'starsoul:suit_slots_elem', type = T.inventoryList}; + guns = {key = 'starsoul:suit_slots_gun', type = T.inventoryList}; + ammo = {key = 'starsoul:suit_slots_ammo', type = T.inventoryList}; +} ADDED mods/starsoul/suit.lua Index: mods/starsoul/suit.lua ================================================================== --- mods/starsoul/suit.lua +++ mods/starsoul/suit.lua @@ -0,0 +1,455 @@ +local lib = starsoul.mod.lib + +local suitStore = starsoul.store.suitMeta +starsoul.item.suit = lib.registry.mk 'starsoul:suits'; + +-- note that this cannot be persisted as a reference to a particular suit in the world +local function suitContainer(stack, inv) + return starsoul.item.container(stack, inv, { + pfx = 'starsoul_suit' + }) +end +starsoul.type.suit = lib.class { + name = 'starsoul:suit'; + construct = function(stack) + return { + item = stack; + inv = suitStore(stack); + } + end; + __index = { + powerState = function(self) + local s = self.item + if not s then return nil end + local m = s:get_meta():get_int('starsoul:power_mode') + if m == 1 then return 'on' + elseif m == 2 then return 'powerSave' + else return 'off' end + end; + powerStateSet = function(self, state) + local s = self.item + if not s then return nil end + local m + if state == 'on' then m = 1 -- TODO check power level + elseif state == 'powerSave' then m = 2 + else m = 0 end + if self:powerLeft() <= 0 then m = 0 end + s:get_meta():set_int('starsoul:power_mode', m) + end; + powerLeft = function(self) + local batteries = self.inv.read 'batteries' + local power = 0 + for idx, slot in pairs(batteries) do + power = power + starsoul.mod.electronics.dynamo.totalPower(slot) + end + return power + end; + powerCapacity = function(self) + local batteries = self.inv.read 'batteries' + local power = 0 + for idx, slot in pairs(batteries) do + power = power + starsoul.mod.electronics.dynamo.initialPower(slot) + end + return power + end; + maxPowerUse = function(self) + local batteries = self.inv.read 'batteries' + local w = 0 + for idx, slot in pairs(batteries) do + w = w + starsoul.mod.electronics.dynamo.dischargeRate(slot) + end + return w + end; + onReconfigure = function(self, inv) + -- apply any changes to item metadata and export any subinventories + -- to the provided invref, as they may have changed + local sc = starsoul.item.container(self.item, inv, {pfx = 'starsoul_suit'}) + sc:push() + self:pullCanisters(inv) + end; + onItemMove = function(self, user, list, act, what) + -- called when the suit inventory is changed + if act == 'put' then + if list == 'starsoul_suit_bat' then + user:suitSound('starsoul-suit-battery-in') + elseif list == 'starsoul_suit_chips' then + user:suitSound('starsoul-suit-chip-in') + elseif list == 'starsoul_suit_canisters' then + user:suitSound('starsoul-insert-snap') + end + elseif act == 'take' then + if list == 'starsoul_suit_bat' then + user:suitSound('starsoul-insert-snap') + elseif list == 'starsoul_suit_chips' then + --user:suitSound('starsoul-suit-chip-out') + elseif list == 'starsoul_suit_canisters' then + user:suitSound('starsoul-insert-snap') + end + end + end; + def = function(self) + return self.item:get_definition()._starsoul.suit + end; + pullCanisters = function(self, inv) + starsoul.item.container.dropPrefix(inv, 'starsoul_canister') + self:forCanisters(inv, function(sc) sc:pull() end) + end; + pushCanisters = function(self, inv, st, i) + self:forCanisters(inv, function(sc) + sc:push() + return true + end) + end; + forCanisters = function(self, inv, fn) + local cans = inv:get_list 'starsoul_suit_canisters' + if cans and next(cans) then for i, st in ipairs(cans) do + if not st:is_empty() then + local pfx = 'starsoul_canister_' .. tostring(i) + local sc = starsoul.item.container(st, inv, {pfx = pfx}) + if fn(sc, st, i, pfx) then + inv:set_stack('starsoul_suit_canisters', i, st) + end + end + end end + end; + establishInventories = function(self, obj) + local inv = obj:get_inventory() + local ct = suitContainer(self.item, inv) + ct:pull() + self:pullCanisters(inv) + + --[[ + local def = self:def() + local sst = suitStore(self.item) + local function readList(listName, prop) + inv:set_size(listName, def.slots[prop]) + if def.slots[prop] > 0 then + local lst = sst.read(prop) + inv:set_list(listName, lst) + end + end + readList('starsoul_suit_chips', 'chips') + readList('starsoul_suit_bat', 'batteries') + readList('starsoul_suit_guns', 'guns') + readList('starsoul_suit_elem', 'elements') + readList('starsoul_suit_ammo', 'ammo') + ]] + end; + }; +} + +-- TODO find a better place for this! +starsoul.type.suit.purgeInventories = function(obj) + local inv = obj:get_inventory() + starsoul.item.container.dropPrefix(inv, 'starsoul_suit') + starsoul.item.container.dropPrefix(inv, 'starsoul_canister') + --[[inv:set_size('starsoul_suit_bat', 0) + inv:set_size('starsoul_suit_guns', 0) + inv:set_size('starsoul_suit_chips', 0) + inv:set_size('starsoul_suit_ammo', 0) + inv:set_size('starsoul_suit_elem', 0) + ]] +end + +starsoul.item.suit.foreach('starsoul:suit-gen', {}, function(id, def) + local icon = lib.image(def.img or 'starsoul-item-suit.png') + + local iconColor = def.iconColor + if not iconColor then + iconColor = (def.tex and def.tex.plate and def.tex.plate.tint) + or def.defaultColor + iconColor = iconColor:to_hsl() + iconColor.lum = 0 + end + + if iconColor then icon = icon:shift(iconColor) end + + if not def.adorn then + function def.adorn(a, item, persona) + local function imageFor(pfx) + return lib.image(string.format("%s-%s-%s.png", pfx, persona.species, persona.speciesVariant)) + end + if not def.tex then return end + a.suit = {} + for name, t in pairs(def.tex) do + local img = imageFor(t.id) + local color + + local cstr = item:get_meta():get_string('starsoul:tint_suit_' .. name) + if cstr and cstr ~= '' then + color = lib.color.unmarshal(cstr) + elseif t.tint then + color = t.tint or def.defaultColor + end + + if color then + local hsl = color:to_hsl() + local adjusted = { + hue = hsl.hue; + sat = hsl.sat * 2 - 1; + lum = hsl.lum * 2 - 1; + } + img = img:shift(adjusted) + end + + a.suit[name] = img + end + end + end + + minetest.register_tool(id, { + short_description = def.name; + description = starsoul.ui.tooltip { + title = def.name; + desc = def.desc; + color = lib.color(.1, .7, 1); + }; + groups = { + suit = 1; + inv = 1; -- has inventories + batteryPowered = 1; -- has a battery inv + programmable = 1; -- has a chip inv + }; + on_use = function(st, luser, pointed) + local user = starsoul.activeUsers[luser:get_player_name()] + if not user then return end + -- have mercy on users who've lost their suits and wound + -- up naked and dying of exposure + if user:naked() then + local ss = st:take_item(1) + user:setSuit(starsoul.type.suit(ss)) + user:suitSound('starsoul-suit-don') + return st + end + end; + inventory_image = icon:render(); + _starsoul = { + container = { + workbench = { + order = {'batteries','chips','guns','ammo'} + }; + list = { + bat = { + key = 'starsoul:suit_slots_bat'; + accept = 'dynamo'; + sz = def.slots.batteries; + }; + chips = { + key = 'starsoul:suit_slots_chips'; + accept = 'chip'; + sz = def.slots.chips; + }; + canisters = { + key = 'starsoul:suit_slots_canisters'; + accept = 'canister'; + sz = def.slots.canisters; + }; + guns = { + key = 'starsoul:suit_slots_gun'; + accept = 'weapon'; + workbench = { + label = 'Weapon'; + icon = 'starsoul-ui-icon-gun'; + color = lib.color(1,0,0); + }; + sz = def.slots.guns; + }; + ammo = { + key = 'starsoul:suit_slots_ammo'; + accept = 'ammo'; + workbench = { + label = 'Ammunition'; + color = lib.color(1,.5,0); + easySlots = true; -- all slots accessible on the go + }; + sz = def.slots.ammo; + }; + }; + }; + event = { + create = function(st,how) + local s = suitStore(st) + -- make sure there's a defined powerstate + starsoul.type.suit(st):powerStateSet 'off' + suitContainer(st):clear() + --[[ populate meta tables + s.write('batteries', {}) + s.write('guns', {}) + s.write('ammo', {}) + s.write('elements', {}) + s.write('chips', {})]] + end; + }; + suit = def; + }; + }); +end) + +local slotProps = { + starsoul_cfg = { + itemClass = 'inv'; + }; + starsoul_suit_bat = { + suitSlot = true; + powerLock = true; + itemClass = 'dynamo'; + }; + starsoul_suit_chips = { + suitSlot = true; + powerLock = true; + itemClass = 'chip'; + }; + starsoul_suit_guns = { + suitSlot = true; + maintenanceNode = ''; + itemClass = 'suitWeapon'; + }; + starsoul_suit_ammo = { + suitSlot = true; + maintenanceNode = ''; + itemClass = 'suitAmmo'; + }; + starsoul_suit_canisters = { + suitSlot = true; + itemClass = 'canister'; + }; +} + +minetest.register_allow_player_inventory_action(function(luser, act, inv, p) + local user = starsoul.activeUsers[luser:get_player_name()] + local function grp(i,g) + return minetest.get_item_group(i:get_name(), g) ~= 0 + end + local function checkBaseRestrictions(list) + local restrictions = slotProps[list] + if not restrictions then return nil, true end + if restrictions.suitSlot then + if user:naked() then return restrictions, false end + end + if restrictions.powerLock then + if user:getSuit():powerState() ~= 'off' then return restrictions, false end + end + return restrictions, true + end + local function itemFits(item, list) + local rst, ok = checkBaseRestrictions(list) + if not ok then return false end + if rst == nil then return true end + + if rst.itemClass and not grp(item, rst.itemClass) then + return false + end + if rst.maintenanceNode then return false end + -- FIXME figure out best way to identify when the player is using a maintenance node + + if grp(item, 'specialInventory') then + if grp(item, 'powder') and list ~= 'starsoul_suit_elem' then return false end + -- FIXME handle containers + if grp(item, 'psi') and list ~= 'starsoul_psi' then return false end + end + + return true + end + local function itemCanLeave(item, list) + local rst, ok = checkBaseRestrictions(list) + if not ok then return false end + if rst == nil then return true end + + if minetest.get_item_group(item:get_name(), 'specialInventory') then + + end + + if rst.maintenanceNode then return false end + return true + end + + if act == 'move' then + local item = inv:get_stack(p.from_list, p.from_index) + if not (itemFits(item, p.to_list) and itemCanLeave(item, p.from_list)) then + return 0 + end + elseif act == 'put' then + if not itemFits(p.stack, p.listname) then return 0 end + elseif act == 'take' then + if not itemCanLeave(p.stack, p.listname) then return 0 end + end + return true +end) + +minetest.register_on_player_inventory_action(function(luser, act, inv, p) + local user = starsoul.activeUsers[luser:get_player_name()] + local function slotChange(slot,a,item) + local s = slotProps[slot] + if slot == 'starsoul_suit' then + user:updateSuit() + if user:naked() then + starsoul.type.suit.purgeInventories(user.entity) + user.power.nano = {} + end + elseif s and s.suitSlot then + local s = user:getSuit() + s:onItemMove(user, slot, a, item) + s:onReconfigure(user.entity:get_inventory()) + user:setSuit(s) + else return end + user:updateHUD() + end + + if act == 'put' or act == 'take' then + local item = p.stack + slotChange(p.listname, act, item) + elseif act == 'move' then + local item = inv:get_stack(p.to_list, p.to_index) + slotChange(p.from_list, 'take', item) + slotChange(p.to_list, 'put', item) + end +end) + +local suitInterval = 2.0 +starsoul.startJob('starsoul:suit-software', suitInterval, function(delta) + local runState = { + pgmsRun = {}; + flags = {}; + } + for id, u in pairs(starsoul.activeUsers) do + if not u:naked() then + local reconfSuit = false + local inv = u.entity:get_inventory() + local chips = inv:get_list('starsoul_suit_chips') + local suitprog = starsoul.mod.electronics.chip.usableSoftware(chips) + for _, prop in pairs(suitprog) do + local s = prop.sw + if s.kind == 'suitPower' and (s.powerKind == 'passive' or s.bgProc) and (not runState.pgmsRun[s]) then + local conf = prop.file.body.conf + local enabled = true + for _, e in ipairs(conf) do + if e.key == 'disable' and e.value == 'yes' then + enabled = false + break + end + end + local fn if s.powerKind == 'passive' + then fn = s.run + else fn = s.bgProc + end + function prop.saveConf(cfg) cfg = cfg or conf + prop.fd:write(cfg) + inv:set_stack('starsoul_suit_chips', prop.chipSlot, prop.fd.chip) + reconfSuit = true + end + function prop.giveItem(st) + u:thrustUpon(st) + end + + if enabled and fn(u, prop, suitInterval, runState) then + runState.pgmsRun[s] = true + end + end + end + if reconfSuit then + u:reconfigureSuit() + end + end + end +end) + ADDED mods/starsoul/terrain.lua Index: mods/starsoul/terrain.lua ================================================================== --- mods/starsoul/terrain.lua +++ mods/starsoul/terrain.lua @@ -0,0 +1,243 @@ +local T = starsoul.translator +local lib = starsoul.mod.lib + +starsoul.terrain = {} +local soilSounds = {} +local grassSounds = {} + +minetest.register_node('starsoul:soil', { + description = T 'Soil'; + tiles = {'default_dirt.png'}; + groups = {dirt = 1}; + drop = ''; + sounds = soilSounds; + _starsoul = { + onDestroy = function() end; + kind = 'block'; + elements = {}; + }; +}) + + +minetest.register_node('starsoul:sand', { + description = T 'Sand'; + tiles = {'default_sand.png'}; + groups = {dirt = 1}; + drop = ''; + sounds = soilSounds; + _starsoul = { + kind = 'block'; + fab = starsoul.type.fab { element = { silicon = 25 } }; + }; +}) +minetest.register_craftitem('starsoul:soil_clump', { + short_description = T 'Soil'; + description = starsoul.ui.tooltip { + title = T 'Soil'; + desc = 'A handful of nutrient-packed soil, suitable for growing plants'; + color = lib.color(0.3,0.2,0.1); + }; + inventory_image = 'starsoul-item-soil.png'; + groups = {soil = 1}; + _starsoul = { + fab = starsoul.type.fab { element = { carbon = 12 / 4 } }; + }; +}) + +function starsoul.terrain.createGrass(def) + local function grassfst(i) + local nextNode = def.name + if i >= 0 then + nextNode = nextNode .. '_walk_' .. tostring(i) + end + return { + onWalk = function(pos) + minetest.set_node_at(pos, def.name .. '_walk_2'); + end; + onDecay = function(pos,delta) + minetest.set_node_at(pos, nextNode); + end; + onDestroy = function(pos) end; + fab = def.fab; + recover = def.recover; + recover_vary = def.recover_vary; + }; + end + local drop = { + max_items = 4; + items = { + { + items = {'starsoul:soil'}, rarity = 2; + tool_groups = { 'shovel', 'trowel' }; + }; + }; + } + minetest.register_node(def.name, { + description = T 'Greengraze'; + tiles = { + def.img .. '.png'; + 'default_dirt.png'; + { + name = 'default_dirt.png^' .. def.img ..'_side.png'; + tileable_vertical = false; + }; + }; + groups = {grass = 1, sub_walk = 1}; + drop = ''; + sounds = grassSounds; + _starsoul = grassfst(2); + }) + for i=2,0,-1 do + local opacity = tostring((i/2.0) * 255) + + minetest.register_node(def.name, { + description = def.desc; + tiles = { + def.img .. '.png^(default_footprint.png^[opacity:'..opacity..')'; + 'default_dirt.png'; + { + name = 'default_dirt.png^' .. def.img ..'_side.png'; + tileable_vertical = false; + }; + }; + groups = {grass = 1, sub_walk = 1, sub_decay = 5}; + drop = ''; + _starsoul = grassfst(i-1); + sounds = grassSounds; + }) + end +end + + +starsoul.terrain.createGrass { + name = 'starsoul:greengraze'; + desc = T 'Greengraze'; + img = 'default_grass'; + fab = starsoul.type.fab { + element = { + carbon = 12; + }; + time = { + shred = 2.5; + }; + }; +} + +for _, w in pairs {false,true} do + minetest.register_node('starsoul:liquid_water' .. (w and '_flowing' or ''), { + description = T 'Water'; + drawtype = 'liquid'; + waving = 3; + tiles = { + { + name = "default_water_source_animated.png"; + backface_culling = false; + animation = { + type = "vertical_frames"; + aspect_w = 16; + aspect_h = 16; + length = 2.0; + }; + }; + { + name = "default_water_source_animated.png"; + backface_culling = true; + animation = { + type = "vertical_frames"; + aspect_w = 16; + aspect_h = 16; + length = 2.0; + }; + }; + }; + use_texture_alpha = 'blend'; + paramtype = 'light'; + walkable = false, pointable = false, diggable = false, buildable_to = true; + is_ground_content = false; + drop = ''; + drowning = 1; + liquidtype = w and 'flowing' or 'source'; + liquid_alternative_flowing = 'starsoul:liquid_water_flowing'; + liquid_alternative_source = 'starsoul:liquid_water'; + liquid_viscosity = 1; + liquid_renewable = true; + liquid_range = 2; + drowning = 40; + post_effect_color = {a=103, r=10, g=40, b=70}; + groups = {water = 3, liquid = 3}; + }); +end + + +starsoul.world.mineral.foreach('starsoul:mineral_generate', {}, function(name,m) + local node = string.format('starsoul:mineral_%s', name) + local grp = {mineral = 1} + minetest.register_node(node, { + description = m.desc; + tiles = m.tiles or + (m.tone and { + string.format('default_stone.png^[colorizehsl:%s:%s:%s', + m.tone.hue, m.tone.sat, m.tone.lum) + }) or {'default_stone.png'}; + groups = grp; + drop = m.rocks or ''; + _starsoul = { + kind = 'block'; + elements = m.elements; + fab = m.fab; + recover = m.recover; + recover_vary = m.recover_vary; + }; + }) + if not m.excludeOre then + local seed = 0 + grp.ore = 1 + for i = 1, #m.name do + seed = seed*50 + string.byte(name, i) + end + minetest.register_ore { + ore = node; + ore_type = m.dist.kind; + wherein = {m.dist.among}; + clust_scarcity = m.dist.rare; + y_max = m.dist.height[1], y_min = m.dist.height[2]; + noise_params = m.dist.noise or { + offset = 28; + scale = 16; + spread = vector.new(128,128,128); + seed = seed; + octaves = 1; + }; + } + end +end) + +starsoul.world.mineral.link('feldspar', { + desc = T 'Feldspar'; + excludeOre = true; + recover = starsoul.type.fab { + time = { + shred = 3; + }; + cost = { + shredPower = 3; + }; + }; + recover_vary = function(rng, ctx) + print('vary!', rng:int(), rng:int(0,10)) + return starsoul.type.fab { + element = { + aluminum = rng:int(0,4); + potassium = rng:int(0,2); + calcium = rng:int(0,2); + } + }; + end; +}) + +-- map generation + +minetest.register_alias('mapgen_stone', 'starsoul:mineral_feldspar') +minetest.register_alias('mapgen_water_source', 'starsoul:liquid_water') +minetest.register_alias('mapgen_river_water_source', 'starsoul:liquid_water') + ADDED mods/starsoul/tiers.lua Index: mods/starsoul/tiers.lua ================================================================== --- mods/starsoul/tiers.lua +++ mods/starsoul/tiers.lua @@ -0,0 +1,233 @@ +local lib = starsoul.mod.lib + +starsoul.world.tier = lib.registry.mk 'starsoul:tier' +local T = starsoul.world.tier +local fab = starsoul.type.fab + +function starsoul.world.tier.fabsum(name, ty) + local dest = fab {} + local t = starsoul.world.tier.db[name] + assert(t, 'reference to nonexisting tier '..name) + if t.super then + dest = dest+starsoul.world.tier.fabsum(t.super, ty)*(t.cost or 1) + end + if t.fabclasses and t.fabclasses[ty] then + dest = dest + t.fabclasses[ty] + end + return dest +end + +function starsoul.world.tier.tech(name, tech) + local t = starsoul.world.tier.db[name] + if t.techs and t.techs[tech] ~= nil then return t.techs[tech] end + if t.super then return starsoul.world.tier.tech(t.super, tech) end + return false +end + +T.meld { + base = { + fabclass = { + electric = fab {metal={copper = 10}}; + suit = fab {element={carbon = 1e3}}; + psi = fab {metal={numinium = 1}}; + bio = fab {element={carbon = 1}}; + }; + + }; -- properties that apply to all tiers + ------------------ + -- tier classes -- + ------------------ + + lesser = { + name = 'Lesser', adj = 'Lesser'; + super = 'base'; + fabclasses = { + basis = fab { + metal = {aluminum=4}; + }; + }; + }; + greater = { + name = 'Greater', adj = 'Greater'; + super = 'base'; + fabclasses = { + basis = fab { + metal = {vanadium=2}; + }; + }; + }; + starsoul = { + name = 'Starsoul', adj = 'Starsoul'; + super = 'base'; + fabclasses = { + basis = fab { + metal = {osmiridium=1}; + }; + }; + }; + forevanished = { + name = 'Forevanished One', adj = 'Forevanished'; + super = 'base'; + fabclasses = { + basis = fab { + metal = {elusium=1}; + }; + }; + }; + + ------------------ + -- Lesser Races -- + ------------------ + + makeshift = { -- regular trash + name = 'Makeshift', adj = 'Makeshift'; + super = 'lesser'; + techs = {tool = true, prim = true, electric = true}; + power = 0.5; + efficiency = 0.3; + reliability = 0.2; + cost = 0.3; + fabclasses = { -- characteristic materials + basis = fab { -- fallback + metal = {iron=3}; + }; + }; + }; + + imperial = { --powerful trash + name = 'Imperial', adj = 'Imperial'; + super = 'lesser'; + techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, hover='ion'}; + power = 2.0; + efficiency = 0.5; + reliability = 0.5; + cost = 1.0; + fabclasses = { + basis = fab { + metal = {steel=2}; + }; + }; + }; + + commune = { --reliability + name = 'Commune', adj = 'Commune'; + super = 'lesser'; + techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, gravitic = true, hover='grav'}; + power = 1.0; + efficiency = 2.0; + reliability = 3.0; + cost = 1.5; + fabclasses = { + basis = fab { + metal = {titanium=1}; + time = {print = 1.2}; -- commune stuff is intricate + }; + }; + }; + + ------------------- + -- Greater Races -- + ------------------- + + + ---------------- + -- Starsouled -- + ---------------- + + suIkuri = { --super-tier + name = 'Su\'ikuri', adj = "Su'ikuruk"; + super = 'starsoul'; + techs = {psi = true, prim = true, bioSuit = true, psiSuit = true}; + power = 1.5; + efficiency = 1.0; + reliability = 3.0; + cost = 2.0; + fabclasses = { + psi = fab { + metal = {numinium = 2.0}; + crystal = {beryllium = 1.0}; + }; + bio = fab { + crystal = {beryllium = 1.0}; + }; + }; + }; + + usukwinya = { --value for 'money'; no weapons; no hovertech (they are birds) + -- NOTA BENE: the ususkwinya *do* have weapons of their own; however, + -- they are extremely restricted and never made available except to a + -- very select number of that species. consequently, usuk players + -- of a certain scenario may have usuk starting weapons, but these must + -- be manually encoded to avoid injecting them into the overall crafting + -- /loot system. because there are so few of these weapons in existence, + -- all so tightly controlled, the odds of the weapons or plans winding + -- up on Farthest Shadow are basically zero unless you bring them yourself + name = 'Usukwinya', adj = 'Usuk'; + super = 'starsoul'; + techs = lib.tbl.set('tool', 'electric', 'electronic', 'suit', 'gravitic'); + power = 2.0; + efficiency = 2.0; + reliability = 2.0; + cost = 0.5; + fabclasses = { + basis = fab { + crystal = {aluminum = 5}; -- ruby + }; + }; + }; + + eluthrai = { --super-tier + name = 'Eluthrai', adj = 'Eluthran'; + super = 'starsoul'; + techs = {tool = true, electric = true, electronic = true, weapon = true, gravitic = true, gravweapon = true, suit = true, combatSuit = true, hover = 'grav'}; + power = 4.0; + efficiency = 4.0; + reliability = 4.0; + cost = 4.0; + fabclasses = { + basis = fab { + crystal = {carbon = 5}; -- diamond + }; + special = fab { + metal = {technetium=1, cinderstone=1} + }; + }; + }; + + ----------------------- + -- Forevanished Ones -- + ----------------------- + + firstborn = { --god-tier + name = 'Firstborn', adj = 'Firstborn'; + super = 'forevanished'; + techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true}; + power = 10.0; + efficiency = 5.0; + reliability = 3.0; + cost = 10.0; + fabclasses = { + basis = fab { + metal = {technetium=2, neodymium=3, sunsteel=1}; + crystal = {astrite=1}; + }; + }; + }; + + forevanisher = { --godslayer-tier + name = 'Forevanisher', adj = 'Forevanisher'; + super = 'forevanished'; + techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true}; + power = 20.0; + efficiency = 1.0; + reliability = 2.0; + cost = 100.0; + fabclasses = { + basis = fab { + metal = {}; + crystal = {}; + }; + }; + }; + +} ADDED mods/starsoul/ui.lua Index: mods/starsoul/ui.lua ================================================================== --- mods/starsoul/ui.lua +++ mods/starsoul/ui.lua @@ -0,0 +1,281 @@ +local lib = starsoul.mod.lib + +starsoul.ui = {} + +starsoul.type.ui = lib.class { + name = 'starsoul:ui'; + __index = { + action = function(self, user, state, fields) + local pg = self.pages[state.page or 'index'] + if not pg then return end + if pg.handle then + local redraw, reset = pg.handle(state, user, fields) + if reset then pg.setupState(state,user) end + if redraw then self:show(user) end + end + if fields.quit then self:cb('onClose', user) end + end; + cb = function(self, name, user, ...) + local state = self:begin(user) + if self[name] then self[name](state, user, ...) end + local pcb = self.pages[state.page][name] + if pcb then pcb(state, user, ...) end + end; + begin = function(self, user, page, ...) + local state = starsoul.activeUI[user.name] + if state and state.form ~= self.id then + state = nil + starsoul.activeUI[user.name] = nil + end + local created = state == nil + + if not state then + state = { + page = page or 'index'; + form = self.id; + } + starsoul.activeUI[user.name] = state + self:cb('setupState', user, ...) + elseif page ~= nil and state.page ~= page then + state.page = page + local psetup = self.pages[state.page].setupState + if psetup then psetup(state,user, ...) end + end + return state, created + end; + render = function(self, state, user) + return self.pages[state.page].render(state, user) + end; + show = function(self, user) + local state = self:begin(user) + minetest.show_formspec(user.name, self.id,self:render(state, user)) + end; + open = function(self, user, page, ...) + user:suitSound 'starsoul-nav' + self:begin(user, page, ...) + self:show(user) + end; + close = function(self, user) + local state = starsoul.activeUI[user.name] + if state and state.form == self.id then + self:cb('onClose', user) + starsoul.activeUI[user.name] = nil + minetest.close_formspec(user.name, self.id) + end + end; + }; + construct = function(p) + if not p.id then error('UI missing id') end + p.pages = p.pages or {} + return p + end; +} + +function starsoul.interface.install(ui) + starsoul.interface.link(ui.id, ui) +end + +function starsoul.ui.build(def, parent) + local clr = def.color + if clr and lib.color.id(clr) then + clr = clr:to_hsl_o() + end + local state = { + x = (def.x or 0); + y = (def.y or 0); + w = def.w or 0, h = def.h or 0; + fixed = def.fixed or false; + spacing = def.spacing or 0; + padding = def.padding or 0; + align = def.align or (parent and parent.align) or 'left'; + lines = {}; + mode = def.mode or (parent and parent.mode or nil); -- hw or sw + gen = (parent and parent.gen or 0) + 1; + fg = def.fg or (parent and parent.fg); + color = clr or (parent and parent.color) or { + hue = 260, sat = 0, lum = 0 + }; + } + local lines = state.lines + local cmod = string.format('^[hsl:%s:%s:%s', + state.color.hue, state.color.sat*0xff, state.color.lum*0xff) + + local E = minetest.formspec_escape + if state.padding/2 > state.x then state.x = state.padding/2 end + if state.padding/2 > state.y then state.y = state.padding/2 end + + local function btnColorDef(sel) + local function climg(state,img) + local selstr + if sel == nil then + selstr = string.format( + 'button%s,' .. + 'button_exit%s,' .. + 'image_button%s,' .. + 'item_image_button%s', + state, state, state, state) + else + selstr = E(sel) .. state + end + + return string.format('%s[%s;' .. + 'bgimg=%s;' .. + 'bgimg_middle=16;' .. + 'content_offset=0,0' .. + ']', sel and 'style' or 'style_type', + selstr, E(img..'^[resize:48x48'..cmod)) + end + + return climg('', 'starsoul-ui-button-sw.png') .. + climg(':hovered', 'starsoul-ui-button-sw-hover.png') .. + climg(':pressed', 'starsoul-ui-button-sw-press.png') + end + local function widget(...) + table.insert(lines, string.format(...)) + end + if def.kind == 'vert' then + for _, w in ipairs(def) do + local src, st = starsoul.ui.build(w, state) + widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src) + state.y=state.y + state.spacing + st.h + state.w = math.max(state.w, st.w) + end + state.w = state.w + state.padding + state.h = state.y + state.padding/2 + elseif def.kind == 'hztl' then + for _, w in ipairs(def) do + local src, st = starsoul.ui.build(w, state) + widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src) + -- TODO alignments + state.x=state.x + state.spacing + st.w + state.h = math.max(state.h, st.h) + end + state.h = state.h + state.padding + state.w = state.x + state.padding/2 + elseif def.kind == 'list' then + local slotTypes = { + plain = {hue = 200, sat = -.1, lum = 0}; + element = {hue = 20, sat = -.3, lum = 0}; + chip = {hue = 0, sat = -1, lum = 0}; + psi = {hue = 300, sat = 0, lum = 0}; + power = {hue = 50, sat = 0, lum = .2}; + } + local img + if state.mode == 'hw' then + img = lib.image('starsoul-ui-slot-physical.png'); + else + img = lib.image('starsoul-ui-slot.png'):shift(slotTypes[def.listContent or 'plain']); + end + local spac = state.spacing + widget('style_type[list;spacing=%s,%s]',spac,spac) + assert(def.w and def.h, 'ui-lists require a fixed size') + for lx = 0, def.w-1 do + for ly = 0, def.h-1 do + local ox, oy = state.x + lx*(1+spac), state.y + ly*(1+spac) + table.insert(lines, string.format('image[%s,%s;1.1,1.1;%s]', ox-0.05,oy-0.05, img:render())) + end end + table.insert(lines, string.format('listcolors[#00000000;#ffffff10]')) -- FIXME + table.insert(lines, string.format('list[%s;%s;%s,%s;%s,%s;%s]', + E(def.target), E(def.inv), + state.x, state.y, + def.w, def.h, + def.idx)) + local sm = 1 + state.w = def.w * sm + (spac * (def.w - 1)) + state.h = def.h * sm + (spac * (def.h - 1)) + elseif def.kind == 'contact' then + if def.color then table.insert(lines, btnColorDef(def.id)) end + widget('image_button%s[%s,%s;%s,%s;%s;%s;%s]', + def.close and '_exit' or '', + state.x, state.y, def.w, def.h, + E(def.img), E(def.id), E(def.label or '')) + elseif def.kind == 'button' then + if def.color then table.insert(lines, btnColorDef(def.id)) end + local label = E(def.label or '') + if state.fg then label = lib.color(state.fg):fmt(label) end + widget('button%s[%s,%s;%s,%s;%s;%s]', + def.close and '_exit' or '', + state.x, state.y, def.w, def.h, + E(def.id), label) + elseif def.kind == 'img' then + widget('%s[%s,%s;%s,%s;%s]', + def.item and 'item_image' or 'image', + state.x, state.y, def.w, def.h, E(def.item or def.img)) + elseif def.kind == 'label' then + local txt = E(def.text) + if state.fg then txt = lib.color(state.fg):fmt(txt) end + widget('label[%s,%s;%s]', + state.x, state.y + def.h*.5, txt) + elseif def.kind == 'text' then + -- TODO paragraph formatter + widget('hypertext[%s,%s;%s,%s;%s;%s]', + state.x, state.y, def.w, def.h, E(def.id), E(def.text)) + elseif def.kind == 'hbar' or def.kind == 'vbar' then -- TODO fancy image bars + local cl = lib.color(state.color) + local fg = state.fg or cl:readable(.8,1) + local wfac, hfac = 1,1 + local clamp = math.min(math.max(def.fac, 0), 1) + if def.kind == 'hbar' + then wfac = wfac * clamp + else hfac = hfac * clamp + end + local x,y, w,h = state.x, state.y, def.w, def.h + widget('box[%s,%s;%s,%s;%s]', + x,y, w,h, cl:brighten(0.2):hex()) + widget('box[%s,%s;%s,%s;%s]', + x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex()) + if def.text then + widget('hypertext[%s,%s;%s,%s;;%s]', + state.x, state.y, def.w, def.h, + string.format('%s', fg:hex(), E(def.text))) + end + end + + if def.desc then + widget('tooltip[%s,%s;%s,%s;%s]', + state.x, state.y, def.w, def.h, E(def.desc)) + end + + local originX = (parent and parent.x or 0) + local originY = (parent and parent.y or 0) + local l = table.concat(lines) + -- if state.fixed and (state.w < state.x or state.h < state.y) then + -- l = string.format('scroll_container[%s,%s;%s,%s;scroll_%s;%s]%sscroll_container_end[]', + -- (parent and parent.x or 0), (parent and parent.y or 0), + -- state.w, state.h, state.gen, + -- (state.x > state.w) and 'horizontal' or 'vertical', l) + -- end + + + if def.mode or def.container then + if def.mode then + l = string.format('background9[%s,%s;%s,%s;%s;false;64]', + originX, originY, state.w, state.h, + E(string.format('starsoul-ui-bg-%s.png%s^[resize:128x128', + (def.mode == 'sw') and 'digital' + or 'panel', cmod))) .. l + end + if parent == nil or state.color ~= parent.color then + l = btnColorDef() .. l + end + end + if not parent then + return string.format('formspec_version[6]size[%s,%s]%s', state.w, state.h, l), state + else + return l, state + end +end + +starsoul.ui.tooltip = lib.ui.tooltipper { + colors = { + -- generic notes + neutral = lib.color(.5,.5,.5); + good = lib.color(.2,1,.2); + bad = lib.color(1,.2,.2); + info = lib.color(.4,.4,1); + -- chip notes + schemaic = lib.color(.2,.7,1); + ability = lib.color(.7,.2,1); + driver = lib.color(1,.7,.2); + }; +} ADDED mods/starsoul/user.lua Index: mods/starsoul/user.lua ================================================================== --- mods/starsoul/user.lua +++ mods/starsoul/user.lua @@ -0,0 +1,873 @@ +-- [ʞ] user.lua +-- ~ lexi hale +-- © EUPL v1.2 +-- ? defines the starsoul.type.user class, which is +-- the main interface between the game world and the +-- client. it provides for initial signup and join, +-- managing the HUD, skinning the player model, +-- effecting weather changes, etc. + +local lib = starsoul.mod.lib + +local function hudAdjustBacklight(img) + local night = math.abs(minetest.get_timeofday() - .5) * 2 + local opacity = night*0.8 + return img:fade(opacity) +end + +local userStore = lib.marshal.metaStore { + persona = { + key = 'starsoul:persona'; + type = starsoul.store.persona; + }; +} + +local suitStore = starsoul.store.suitMeta + +starsoul.type.user = lib.class { + name = 'starsoul:user'; + construct = function(ident) + local name, luser + if type(ident) == 'string' then + name = ident + luser = minetest.get_player_by_name(name) + else + luser = ident + name = luser:get_player_name() + end + return { + entity = luser; + name = name; + hud = { + elt = {}; + }; + tree = {}; + action = { + bits = 0; -- for control deltas + prog = {}; -- for recording action progress on a node; reset on refocus + tgt = {type='nothing'}; + sfx = {}; + fx = {}; + }; + actMode = 'off'; + power = { + nano = {primary = nil, secondary = nil}; + weapon = {primary = nil, secondary = nil}; + psi = {primary = nil, secondary = nil}; + maneuver = nil; + }; + pref = { + calendar = 'commune'; + }; + } + end; + __index = { + pullPersona = function(self) + -- if later records are added in public updates, extend this function to merge them + -- into one object + local s = userStore(self.entity) + self.persona = s.read 'persona' + end; + pushPersona = function(self) + local s = userStore(self.entity) + s.write('persona', self.persona) + end; + uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end; + statDelta = function(self, stat, d, cause, abs) + local dt = self.persona.statDeltas + local base + if abs then + local min, max + min, max, base = self:statRange(stat) + if d == true then d = max + elseif d == false then d = min end + end + if stat == 'health' then + self.entity:set_hp(abs and d or (self.entity:get_hp() + d), cause) + elseif stat == 'breath' then + self.entity:set_breath(abs and d or (self.entity:get_breath() + d)) + else + if abs then + dt[stat] = d - base + else + dt[stat] = dt[stat] + d + end + self:pushPersona() + end + self:updateHUD() + -- TODO trigger relevant animations? + end; + lookupSpecies = function(self) + return starsoul.world.species.lookup(self.persona.species, self.persona.speciesVariant) + end; + phenoTrait = function(self, trait) + local s,v = self:lookupSpecies() + return v.traits[trait] or s.traits[trait] or 0 + end; + statRange = function(self, stat) --> min, max, base + return starsoul.world.species.statRange( + self.persona.species, self.persona.speciesVariant, stat) + end; + effectiveStat = function(self, stat) + local val + local min, max, base = self:statRange(stat) + + if stat == 'health' then + val = self.entity:get_hp() + elseif stat == 'breath' then + val = self.entity:get_breath() + else + val = base + self.persona.statDeltas[stat] or 0 + end + + local d = max - min + return val, (val - min) / d + end; + damageModifier = function(self, kind, amt) + if kind == 'bluntForceTrauma' then + local std = self:phenoTrait 'sturdiness' + if std < 0 then + amt = amt / 1+std + else + amt = amt * 1-std + end + end + return amt + end; + attachImage = function(self, def) + local user = self.entity + local img = {} + img.id = user:hud_add { + type = 'image'; + text = def.tex; + scale = def.scale; + alignment = def.align; + position = def.pos; + offset = def.ofs; + z_index = def.z; + } + if def.update then + img.update = function() + def.update(user, function(prop, val) + user:hud_change(img.id, prop, val) + end, def) + end + end + return img + end; + attachMeter = function(self, def) + local luser = self.entity + local m = {} + local w = def.size or 80 + local szf = w / 80 + local h = szf * 260 + m.meter = luser:hud_add { + type = 'image'; + scale = {x = szf, y = szf}; + alignment = def.align; + position = def.pos; + offset = def.ofs; + z_index = def.z or 0; + } + local cx = def.ofs.x + (w/2)*def.align.x + local cy = def.ofs.y + (h/2)*def.align.y + local oy = cy + h/2 - 42 + -- this is so fucking fragile holy fuck + m.readout = luser:hud_add { + type = 'text'; + scale = {x = w, y = h}; + size = szf; + style = 4; + position = def.pos; + alignment = {x=0,0}; + offset = {x = cx, y = oy}; + z_index = (def.z or 0)+1; + number = 0xffffff; + } + m.destroy = function() + luser:hud_remove(m.meter) + luser:hud_remove(m.readout) + end + m.update = function() + local v,txt,color,txtcolor = def.measure(luser,def) + v = math.max(0, math.min(1, v)) + local n = math.floor(v*16) + 1 + local img = hudAdjustBacklight(lib.image('starsoul-ui-meter.png')) + :colorize(color or def.color) + if def.flipX then + img = img:transform 'FX' + end + img = img:render() + img = img .. '^[verticalframe:17:' .. tostring(17 - n) + luser:hud_change(m.meter, 'text', img) + if txt then + luser:hud_change(m.readout, 'text', txt) + end + if txtcolor then + luser:hud_change(m.readout, 'number', txtcolor:hex()) + end + end + return m + end; + attachTextBox = function(self, def) + local luser = self.entity + local box = {} + box.id = luser:hud_add { + type = 'text'; + text = ''; + alignment = def.align; + number = def.color and def.color:int24() or 0xFFffFF; + scale = def.bound; + size = {x = def.size, y=0}; + style = def.style; + position = def.pos; + offset = def.ofs; + } + box.update = function() + local text, color = def.text(self, box, def) + luser:hud_change(box.id, 'text', text) + if color then + luser:hud_change(box.id, 'number', color:int24()) + end + end + return box + end; + attachStatBar = function(self, def) + local luser = self.entity + local bar = {} + local img = lib.image 'starsoul-ui-bar.png' + local colorized = img + if type(def.color) ~= 'function' then + colorized = colorized:shift(def.color) + end + + bar.id = luser:hud_add { + type = 'statbar'; + position = def.pos; + offset = def.ofs; + name = def.name; + text = colorized:render(); + text2 = img:tint{hue=0, sat=-1, lum = -0.5}:fade(0.5):render(); + number = def.size; + item = def.size; + direction = def.dir; + alignment = def.align; + size = {x=4,y=24}; + } + bar.update = function() + local sv, sf = def.stat(self, bar, def) + luser:hud_change(bar.id, 'number', def.size * sf) + if type(def.color) == 'function' then + local clr = def.color(sv, luser, sv, sf) + luser:hud_change(bar.id, 'text', img:tint(clr):render()) + end + end + return bar, {x=3 * def.size, y=16} -- x*2??? what + end; + createHUD = function(self) + local function basicStat(statName) + return function(user, bar) + return self:effectiveStat(statName) + end + end + local function batteryLookup(user) + local max = user:suitPowerCapacity() + if max == 0 then return 0, 0 end + local ch = user:suitCharge() + return (ch/max)*100, ch/max + end + local function C(h,s,l) return {hue=h,sat=s,lum=l} end + local hbofs = (1+self.entity:hud_get_hotbar_itemcount()) * 25 + local bpad = 8 + self.hud.elt.health = self:attachStatBar { + name = 'health', stat = basicStat 'health'; + color = C(340,0,.3), size = 100; + pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-48 - bpad}; + dir = 1; + align = {x=-1, y=-1}; + } + self.hud.elt.stamina = self:attachStatBar { + name = 'stamina', stat = basicStat 'stamina'; + color = C(60,0,.2), size = 100; + pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-24 - bpad}; + dir = 1; + align = {x=-1, y=-1}; + } + self.hud.elt.bat = self:attachStatBar { + name = 'battery', stat = batteryLookup; + color = C(190,0,.2), size = 100; + pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad}; + dir = 0; + align = {x=1, y=-1}; + } + self.hud.elt.psi = self:attachStatBar { + name = 'psi', stat = basicStat 'psi'; + color = C(320,0,.2), size = 100; + pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad}; + dir = 0; + align = {x=1, y=-1}; + } + self.hud.elt.time = self:attachTextBox { + name = 'time'; + align = {x=0, y=1}; + pos = {x=0.5, y=1}; + ofs = {x=0,y=-95}; + text = function(user) + local cal = starsoul.world.time.calendar[user.pref.calendar] + return cal.time(minetest.get_timeofday()) + end; + } + self.hud.elt.temp = self:attachMeter { + name = 'temp'; + align = {x=1, y=-1}; + pos = {x=0, y=1}; + ofs = {x=20, y=-20}; + measure = function(user) + local warm = self:effectiveStat 'warmth' + local n, color if warm < 0 then + n = math.min(100, -warm) + color = lib.color(0.1,0.3,1):lerp(lib.color(0.7, 1, 1), math.min(1, n/50)) + else + n = math.min(100, warm) + color = lib.color(0.1,0.3,1):lerp(lib.color(1, 0, 0), math.min(1, n/50)) + end + local txt = string.format("%s°", math.floor(warm)) + return (n/50), txt, color + end; + } + self.hud.elt.geiger = self:attachMeter { + name = 'geiger'; + align = {x=-1, y=-1}; + pos = {x=1, y=1}; + ofs = {x=-20, y=-20}; + flipX = true; + measure = function(user) + local hot = self:effectiveStat 'irradiation' + local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5)) + local txt = string.format("%sGy", math.floor(hot)) + return (hot/5), txt, color + end; + } + self.hud.elt.crosshair = self:attachImage { + name = 'crosshair '; + tex = ''; + pos = {x=.5, y=.5}; + scale = {x=1,y=1}; + ofs = {x=0, y=0}; + align = {x=0, y=0}; + update = function(user, set) + local imgs = { + off = ''; + nano = 'starsoul-ui-crosshair-nano.png'; + psi = 'starsoul-ui-crosshair-psi.png'; + weapon = 'starsoul-ui-crosshair-weapon.png'; + } + set('text', imgs[self.actMode] or imgs.off) + end; + }; + local hudCenterBG = lib.image 'starsoul-ui-hud-bg.png':colorize(self:uiColor()) + self.hud.elt.bg = self:attachImage { + name = 'hudBg'; + tex = hudCenterBG:render(); + pos = {x=.5, y=1}; + scale = {x=1,y=1}; + ofs = {x=0, y=0}; + align = {x=0, y=-1}; + z = -1; + update = function(user, set) + set('text', hudAdjustBacklight(hudCenterBG):render()) + end; + }; + end; + onModeChange = function(self, oldMode, silent) + self.hud.elt.crosshair.update() + if not silent then + local sfxt = { + off = 'starsoul-mode-off'; + nano = 'starsoul-mode-nano'; + psi = 'starsoul-mode-psi'; + weapon = 'starsoul-mode-weapon'; + } + local sfx = self.actMode and sfxt[self.actMode] or sfxt.off + self:suitSound(sfx) + end + end; + actModeSet = function(self, mode, silent) + if not mode then mode = 'off' end + local oldMode = self.actMode + self.actMode = mode + self:onModeChange(oldMode, silent) + if mode ~= oldMode then + starsoul.ui.setupForUser(self) + end + end; + deleteHUD = function(self) + for name, e in pairs(self.hud.elt) do + self:hud_delete(e.id) + end + end; + updateHUD = function(self) + for name, e in pairs(self.hud.elt) do + if e.update then e.update() end + end + end; + clientInfo = function(self) + return minetest.get_player_information(self.name) + end; + onSignup = function(self) + local meta = self.entity:get_meta() + local inv = self.entity:get_inventory() + -- the sizes indicated here are MAXIMA. limitations on e.g. the number of elements that may be carried are defined by your suit and enforced through callbacks and UI generation code, not inventory size + inv:set_size('main', 6) -- carried items and tools. main hotbar. + + inv:set_size('starsoul_suit', 1) -- your environment suit (change at wardrobe) + inv:set_size('starsoul_cfg', 1) -- the item you're reconfiguring / container you're accessing + + local scenario + for _, e in pairs(starsoul.world.scenario) do + if e.id == starsoul.world.defaultScenario then + scenario = e break + end + end assert(scenario) + self.persona = starsoul.world.species.birth(scenario.species, scenario.speciesVariant, self.entity) + self.persona.name = self.entity:get_player_name() -- a reasonable default + self.persona.background = starsoul.world.defaultScenario + self:pushPersona() + + local gifts = scenario.startingItems + local inv = self.entity:get_inventory() + inv:set_stack('starsoul_suit', 1, starsoul.item.mk(gifts.suit, self, {gift=true})) + self:getSuit():establishInventories(self.entity) + + local function giveGifts(name, list) + if inv:get_size(name) > 0 then + for i, e in ipairs(list) do + inv:add_item(name, starsoul.item.mk(e, self, {gift=true})) + end + end + end + + giveGifts('starsoul_suit_bat', gifts.suitBatteries) + giveGifts('starsoul_suit_chips', gifts.suitChips) + giveGifts('starsoul_suit_guns', gifts.suitGuns) + giveGifts('starsoul_suit_ammo', gifts.suitAmmo) + giveGifts('starsoul_suit_canisters', gifts.suitCans) + + giveGifts('main', gifts.carry) + + self:reconfigureSuit() + + -- i feel like there has to be a better way + local cx = math.random(-500,500) + local startPoint + repeat local temp = -100 + local cz = math.random(-500,500) + local cy = minetest.get_spawn_level(cx, cz) + if cy then + startPoint = vector.new(cx,cy,cz) + temp = starsoul.world.climate.eval(startPoint,.5,.5).surfaceTemp + end + if cx > 10000 then break end -- avoid infiniloop in pathological conditions + until temp > -2 + self.entity:set_pos(startPoint) + meta:set_string('starsoul_spawn', startPoint:to_string()) + end; + onDie = function(self, reason) + local inv = self.entity:get_inventory() + local where = self.entity:get_pos() + local function dropInv(lst) + local l = inv:get_list(lst) + for i, o in ipairs(l) do + if o and not o:is_empty() then + minetest.item_drop(o, self.entity, where) + end + end + inv:set_list(lst, {}) + end + dropInv 'main' + dropInv 'starsoul_suit' + self:statDelta('psi', 0, 'death', true) + self:statDelta('hunger', 0, 'death', true) + self:statDelta('thirst', 0, 'death', true) + self:statDelta('fatigue', 0, 'death', true) + self:statDelta('stamina', 0, 'death', true) + self:updateSuit() + end; + onRespawn = function(self) + local meta = self.entity:get_meta() + self.entity:set_pos(vector.from_string(meta:get_string'starsoul_spawn')) + self:updateSuit() + return true + end; + onJoin = function(self) + local me = self.entity + local meta = me:get_meta() + self:pullPersona() + + -- formspec_version and real_coordinates are apparently just + -- completely ignored here + me:set_formspec_prepend [[ + bgcolor[#00000000;true] + style_type[button,button_exit,image_button,item_image_button;border=false] + style_type[button;bgimg=starsoul-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2] + style_type[button:hovered;bgimg=starsoul-ui-button-hw-hover.png;bgimg_middle=8] + style_type[button:pressed;bgimg=starsoul-ui-button-hw-press.png;bgimg_middle=8;content_offset=0,1] + ]] + local hotbarSlots = me:get_inventory():get_size 'main'; +-- local slotTex = 'starsoul-ui-slot.png' +-- local hbimg = string.format('[combine:%sx128', 128 * hotbarSlots) +-- for i = 0, hotbarSlots-1 do +-- hbimg = hbimg .. string.format(':%s,0=%s', 128 * i, slotTex) +-- end + --me:hud_set_hotbar_image(lib.image(hbimg):colorize(self:uiColor()):fade(.36):render()) +-- me:hud_set_hotbar_selected_image(lib.image(slotTex):colorize(self:uiColor()):render()) + me:hud_set_hotbar_image('[fill:1x24:0,0:' .. self:uiColor():fade(.1):hex()) + me:hud_set_hotbar_selected_image( + '[fill:1x24,0,0:' .. self:uiColor():fade(.4):hex() .. '^[fill:1x1:0,23:#ffFFffff' + ) + me:hud_set_hotbar_itemcount(hotbarSlots) + me:hud_set_flags { + hotbar = true; + healthbar = false; + breathbar = false; + basic_debug = false; + crosshair = false; + } + -- disable builtin crafting + local inv = me:get_inventory() + inv:set_size('craftpreview', 0) + inv:set_size('craftresult', 0) + inv:set_size('craft', 0) + + me:set_stars { + day_opacity = 0.7; + } + me:set_sky { + sky_color = { + day_sky = '#a7c2cd', day_horizon = '#ddeeff'; + dawn_sky = '#003964', dawn_horizon = '#87ebff'; + night_sky = '#000000', night_horizon = '#000E29'; + fog_sun_tint = '#72e4ff'; + fog_moon_tint = '#2983d0'; + fog_tint_type = 'custom'; + }; + fog = { -- not respected?? + -- TODO make this seasonal & vary with weather + fog_distance = 40; + fog_start = 0.3; + }; + } + me:set_sun { + texture = 'starsoul-sun.png'; + sunrise = 'sunrisebg.png^[hsl:180:1:.7'; + tonemap = 'sun_tonemap.png^[hsl:180:1:.7'; + scale = 0.8; + } + me:set_lighting { + shadows = { + intensity = .5; + }; + exposure = { + luminance_max = 3.0; + speed_dark_bright = 0.5; + speed_bright_dark = 1.0; + }; + volumetric_light = { + strength = 0.3; + }; + } + me:set_eye_offset(nil, vector.new(3,-.2,10)) + -- TODO set_clouds speed in accordance with wind + starsoul.world.species.setupEntity(me, self.persona) + starsoul.ui.setupForUser(self) + self:createHUD() + self:updateSuit() + end; + suitStack = function(self) + return self.entity:get_inventory():get_stack('starsoul_suit', 1) + end; + suitSound = function(self, sfx) + -- trigger a sound effect from the player's suit computer + minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true) + end; + suitPowerStateSet = function(self, state, silent) + -- necessary to enable reacting to power state changes + -- e.g. to play sound effects, display warnings + local os + self:forSuit(function(s) + os=s:powerState() + s:powerStateSet(state) + end) + if state == 'off' then + if self.actMode == 'nano' or self.actMode == 'weapon' then + self:actModeSet('off', silent) + end + end + if not silent and os ~= state then + local sfx + if state == 'off' then + sfx = 'starsoul-power-down' + elseif os == 'off' then + sfx = 'starsoul-power-up' + elseif state == 'powerSave' or os == 'powerSave' then + sfx = 'starsoul-configure' + end + if sfx then self:suitSound(sfx) end + end + end; + species = function(self) + return starsoul.world.species.index[self.persona.species] + end; + updateBody = function(self) + local adornment = {} + local suitStack = self:suitStack() + if suitStack and not suitStack:is_empty() then + local suit = suitStack:get_definition()._starsoul.suit + suit.adorn(adornment, suitStack, self.persona) + end + starsoul.world.species.updateTextures(self.entity, self.persona, adornment) + end; + updateSuit = function(self) + self:updateBody() + local inv = self.entity:get_inventory() + local sst = suitStore(self:suitStack()) + if self:naked() then + starsoul.type.suit.purgeInventories(self.entity) + if self.actMode == 'nano' or self.actMode == 'weapon' then + self:actModeSet 'off' + end + else + local suit = self:getSuit() + suit:establishInventories(self.entity) + + if self:suitCharge() <= 0 then + self:suitPowerStateSet 'off' + end + end + self:updateHUD() + end; + reconfigureSuit = function(self) + -- and here's where things get ugly + -- you can't have an inventory inside another item. to hack around this, + -- we use the player as the location of the suit inventories, and whenever + -- there's a change in the content of these inventories, this function is + -- called to serialize those inventories out to the suit stack + if self:naked() then return end + local suit = self:getSuit() + suit:onReconfigure(self.entity:get_inventory()) + self:setSuit(suit) + + -- reconfiguring the suit can affect player abilities: e.g. removing + -- / inserting a chip with a minimap program + end; + getSuit = function(self) + local st = self:suitStack() + if st:is_empty() then return nil end + return starsoul.type.suit(st) + end; + setSuit = function(self, suit) + self.entity:get_inventory():set_stack('starsoul_suit', 1, suit.item) + end; + changeSuit = function(self, ...) + self:setSuit(...) + self:updateSuit() + end; + forSuit = function(self, fn) + local s = self:getSuit() + if fn(s) ~= false then + self:setSuit(s) + end + end; + suitPowerCapacity = function(self) -- TODO optimize + if self:naked() then return 0 end + return self:getSuit():powerCapacity() + end; + suitCharge = function(self) -- TODO optimize + if self:naked() then return 0 end + return self:getSuit():powerLeft() + end; + suitDrawCurrent = function(self, power, time, whatFor, min) + if self:naked() then return 0,0 end + local inv = self.entity:get_inventory() + local bl = inv:get_list('starsoul_suit_bat') + local supply = 0 + local wasteHeat = 0 --TODO handle internally + for slot, ps in ipairs(bl) do + if not ps:is_empty() then + local p, h = starsoul.mod.electronics.dynamo.drawCurrent(ps, power - supply, time) + supply = supply + p + wasteHeat = wasteHeat + h + if power-supply <= 0 then break end + end + end + if min and supply < min then return 0,0 end + inv:set_list('starsoul_suit_bat', bl) + self:reconfigureSuit() + if whatFor then + -- TODO display power use icon + end + return supply, wasteHeat + end; + naked = function(self) + return self:suitStack():is_empty() + end; + onPart = function(self) + starsoul.liveUI [self.name] = nil + starsoul.activeUI [self.name] = nil + starsoul.activeUsers[self.name] = nil + end; + openUI = function(self, id, page, ...) + local ui = assert(starsoul.interface.db[id]) + ui:open(self, page, ...) + end; + onRespond = function(self, ui, state, resp) + ui:action(self, state, resp) + end; + + updateWeather = function(self) + end; + + canInteract = function(self, with) + return true; -- TODO + end; + + trigger = function(self, which, how) + --print('trigger', which, dump(how)) + local p + local wld = self.entity:get_wielded_item() + if which == 'maneuver' then + p = self.power.maneuver + elseif which == 'retarget' then + self.action.prog = {} + elseif wld and not wld:is_empty() then + local wdef = wld:get_definition() + if wdef._starsoul and wdef._starsoul.tool then + p = {tool = wdef._starsoul.tool} + end + elseif self.actMode ~= 'off' then + p = self.power[self.actMode][which] + end + if p == nil then return false end + local ctx, run = { + how = how; + } + if p.chipID then + local inv = self.entity:get_inventory() + local chips = inv:get_list 'starsoul_suit_chips' + for chSlot, ch in pairs(chips) do + if ch and not ch:is_empty() then + local d = starsoul.mod.electronics.chip.read(ch) + if d.uuid == p.chipID then + local pgm = assert(d.files[p.pgmIndex], 'file missing for ability') + ctx.file = starsoul.mod.electronics.chip.fileHandle(ch, p.pgmIndex) + ctx.saveChip = function() + inv:set_slot('starsoul_suit_chips', chSlot, ch) + end + local sw = starsoul.item.sw.db[pgm.body.pgmId] + run = assert(sw.run, 'missing run() for active software ability ' .. pgm.body.pgmId) + break + end + end + end + else + error('bad ability pointer ' .. dump(p)) + end + if run then + run(self, ctx) + return true + end + return false + end; + give = function(self, item) + local inv = self.entity:get_inventory() + local function is(grp) + return minetest.get_item_group(item:get_name(), grp) ~= 0 + end + -- TODO notif popups + if is 'specialInventory' then + if is 'powder' then + if self:naked() then return item end + local cans = inv:get_list 'starsoul_suit_canisters' + if cans and next(cans) then for i, st in ipairs(cans) do + local lst = string.format('starsoul_canister_%u_elem', i) + item = inv:add_item(lst, item) + if item:is_empty() then break end + end end + self:forSuit(function(x) x:pushCanisters(inv) end) + end + return item + else + return inv:add_item('main', item) + end + end; + thrustUpon = function(self, item) + local r = self:give(st) + if not r:is_empty() then + return minetest.add_item(self.entity:get_pos(), r) + end + end; + }; +} + +local biointerval = 3.0 +starsoul.startJob('starsoul:bio', biointerval, function(delta) + for id, u in pairs(starsoul.activeUsers) do + + end +end) + +local cbit = { + up = 0x001; + down = 0x002; + left = 0x004; + right= 0x008; + jump = 0x010; + manv = 0x020; + snk = 0x040; + dig = 0x080; + put = 0x100; + zoom = 0x200; +} +-- this is the painful part +minetest.register_globalstep(function(delta) + local doNothing,mustInit,mustHalt = 0,1,2 + for id, user in pairs(starsoul.activeUsers) do + local ent = user.entity + local bits = ent:get_player_control_bits() + + local function what(b) + if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then + return mustInit + elseif bit.band(bits, b) == 0 and bit.band(user.action.bits, b) ~= 0 then + return mustHalt + else return doNothing end + end + local skipBits = 0 + if user.action.bits ~= bits then + local mPrimary = what(cbit.dig) + local mSecondary = what(cbit.put) + if mPrimary == mustInit then -- ENGINE-BUG + user.action.tgt = {type='nothing'} + user.action.prog = {} + elseif mPrimary == mustHalt then + user:trigger('primary', {state='halt'}) + end + if mSecondary == mustHalt then + user:trigger('secondary', {state='halt'}) + end + end + --bits = bit.band(bits, bit.bnot(skipBits)) + if bit.band(bits, cbit.dig)~=0 then + user:trigger('primary', {state='prog', delta=delta}) + end + if bit.band(bits, cbit.put)~=0 then + user:trigger('secondary', {state='prog', delta=delta}) + end + user.action.bits = bits + -- ENGINE-BUG: dig and put are not handled equally in the + -- engine. it is possible for the put bit to get stuck on + -- if the key is hammered while the player is not moving. + -- the bit will release as soon as the player looks or turns + -- nonetheless this is obnoxious + end +end) ADDED mods/starsoul/world.lua Index: mods/starsoul/world.lua ================================================================== --- mods/starsoul/world.lua +++ mods/starsoul/world.lua @@ -0,0 +1,197 @@ +local lib = starsoul.mod.lib +local world = starsoul.world + +function world.date() + local days = minetest.get_day_count() + local year = math.floor(days / world.planet.orbit); + local day = days % world.planet.orbit; + return { + year = year, day = day; + season = day / world.planet.orbit; + } +end +local lerp = lib.math.lerp + +local function gradient(grad, pos) + local n = #grad + if n == 1 then return grad[1] end + local op = pos*(n-1) + local idx = math.floor(op) + local t = op-idx + return lerp(t, grad[1 + idx], grad[2 + idx]) +end + +local altitudeCooling = 10 / 100 + +-- this function provides the basis for temperature calculation, +-- which is performed by adding this value to the ambient temperature, +-- determined by querying nearby group:heatSource items in accordance +-- with the inverse-square law +function world.climate.eval(pos, tod, season) + local data = minetest.get_biome_data(pos) + local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)] + local heat, humid = data.heat, data.humidity + tod = tod or minetest.get_timeofday() + heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta) + + local td = world.date() + heat = heat + gradient(biome.seasonalTemp, season or td.season) + if pos.y > 0 then + heat = heat - pos.y*altitudeCooling + end + + return { + surfaceTemp = heat; + waterTemp = heat + biome.waterTempDelta; + surfaceHumid = humid; + } +end + +local vdsq = lib.math.vdsq +function world.climate.temp(pos) --> irradiance at pos in W + local cl = world.climate.eval(pos) + local radCenters = starsoul.region.radiator.store:get_areas_for_pos(pos, false, true) + local irradiance = 0 + for _,e in pairs(radCenters) do + local rpos = minetest.string_to_pos(e.data) + local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name]) + local rc = rdef._starsoul.radiator + local r_max = rc.radius(rpos) + + local dist_sq = vdsq(rpos,pos) + if dist_sq <= r_max^2 then + -- cheap bad way + -- if minetest.line_of_sight(rpos,pos) then + -- + -- expensive way + local obstruct = 0 + local ray = Raycast(rpos, pos, true, true) + for p in ray do + if p.type == 'node' then obstruct = obstruct + 1 end + end + + if obstruct < 4 then + local power, customFalloff = rc.radiate(rpos, pos) + -- okay this isn't the real inverse square law but i + -- couldn't figure out a better way to simplify the + -- model without checking an ENORMOUS number of nodes + -- maybe someone else who isn't completely + -- mathtarded can do better. + if not customFalloff then + power = power * (1 - (dist_sq / ((r_max+1)^2))) + end + power = power * (1 - (obstruct/5)) + irradiance = irradiance + power + end + end + end + return irradiance + cl.surfaceTemp +end + +world.ecology.biomes.foreach('starsoul:biome-gen', {}, function(id, b) + b.def.name = id + minetest.register_biome(b.def) +end) + +world.ecology.biomes.link('starsoul:steppe', { + nightTempDelta = -30; + waterTempDelta = 0; + -- W Sp Su Au W + seasonalTemp = {-50, -10, 5, 5, -20, -50}; + def = { + node_top = 'starsoul:greengraze', depth_top = 1; + node_filler = 'starsoul:soil', depth_filler = 4; + node_riverbed = 'starsoul:sand', depth_riverbed = 4; + y_min = 0; + y_max = 512; + heat_point = 10; + humidity_point = 30; + }; +}) + +world.ecology.biomes.link('starsoul:ocean', { + nightTempDelta = -35; + waterTempDelta = 5; + seasonalTemp = {0}; -- no seasonal variance + def = { + y_max = 3; + y_min = -512; + heat_point = 15; + humidity_point = 50; + node_top = 'starsoul:sand', depth_top = 1; + node_filler = 'starsoul:sand', depth_filler = 3; + }; +}) + +local toward = lib.math.toward +local hfinterval = 1.5 +starsoul.startJob('starsoul:heatflow', hfinterval, function(delta) + + -- our base thermal conductivity (κ) is measured in °C/°C/s. say the + -- player is in -30°C weather, and has an internal temperature of + -- 10°C. then: + -- κ = .1°C/C/s (which is apparently 100mHz) + -- Tₚ = 10°C + -- Tₑ = -30°C + -- d = Tₑ − Tₚ = -40°C + -- ΔT = κ×d = -.4°C/s + -- our final change in temperature is computed as tΔC where t is time + local kappa = .05 + for name,user in pairs(starsoul.activeUsers) do + local tr = user:species().tempRange + local t = starsoul.world.climate.temp(user.entity:get_pos()) + local insul = 0 + local naked = user:naked() + local suitDef + if not naked then + suitDef = user:suitStack():get_definition() + insul = suitDef._starsoul.suit.temp.insulation + end + + local warm = user:effectiveStat 'warmth' + local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2] + local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2] + + local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval + local tgt = warm + tDelta + + -- old logic: we move the user towards the exterior temperature, modulated + -- by her suit insulation. + --local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul)) + + if not naked then + local suit = user:getSuit() + local suitPower = suit:powerState() + local suitPowerLeft = suit:powerLeft() + if suitPower ~= 'off' then + local coilPower = 1.0 + local st = suitDef._starsoul.suit.temp + if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end + if tgt < tComfMin and st.maxHeat > 0 then + local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval) + tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval + end + if tgt > tComfMax and st.maxCool > 0 then + local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval) + tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval + end + end + end + + user:statDelta('warmth', tgt - warm) -- dopey but w/e + + warm = tgt -- for the sake of readable code + + if warm < tSafeMin or warm > tSafeMax then + local dv + if warm < tSafeMin then + dv = math.abs(warm - tSafeMin) + else + dv = math.abs(warm - tSafeMax) + end + -- for every degree of difference you suffer 2 points of damage/s + local dmg = math.ceil(dv * 2) + user:statDelta('health', -dmg) + end + end +end) ADDED mods/vtlib/class.lua Index: mods/vtlib/class.lua ================================================================== --- mods/vtlib/class.lua +++ mods/vtlib/class.lua @@ -0,0 +1,63 @@ +return function(meta) + local class = { + id = function(instance) + return getmetatable(instance) == meta + end; + + merge = function(dest, src) + for k,v in pairs(src) do + dest[k] = v + end + if meta.clone then meta.clone(dest) end + end; + } + class.clone = function(instance) + local new = {} + class.merge(new, instance) + setmetatable(new, meta) + return new + end + class.change = function(orig, delta) + local new = class.clone(orig) + class.merge(new, delta) + return new + end + class.mk = function(...) + local new + if #{...} == 1 then + if class.id((...)) then + -- default copy constructor + return class.clone((...)) + elseif meta.cast then + if type((...)) == 'table' then + for from, conv in pairs(meta.cast) do + if from.id and from.id((...)) then + new = conv((...)) + goto setup + end + end + else + local conv = meta.cast[type((...))] + if conv then + new = conv((...)) + goto setup + else assert(false) end + end + end + end + if meta.construct then + new = meta.construct(...) + else -- if there is no constructor, this 'class' is simply a bundle + -- of behaviors for an arbitrary struct + new = (...) + end + + ::setup:: + setmetatable(new, meta) + return new + end + setmetatable(class, { __call = function(self, ...) + return self.mk(...) + end }) + return class +end ADDED mods/vtlib/color.lua Index: mods/vtlib/color.lua ================================================================== --- mods/vtlib/color.lua +++ mods/vtlib/color.lua @@ -0,0 +1,324 @@ +local lib = ... + +local color +local function warp(f) + return function(self, ...) + local n = color(self) + f(n, ...) + return n + end; +end + +local function clip(v) + return math.max(0,math.min(255,v)) +end +local colorStruct do local T,G = lib.marshal.t, lib.marshal.g + colorStruct = G.struct { + red = T.u8; + green = T.u8; + blue = T.u8; + alpha = T.u8; + } +end + +local function from_hsl(hsl, alpha) + -- Based on the algorithm in Computer Graphics: Principles and Practice, by + -- James D. Foley et. al., 2nd ed., p. 596 + -- Degree version, though radian is more natural, I don't want to translate it yet + local h = hsl.hue + local s = hsl.sat + local l = hsl.lum + local value = function(n1, n2, hue) + if hue > 360 then + hue = hue - 360 + elseif hue < 0 then + hue = hue + 360 + end + if hue < 60 then + return n1 + (n2 - n1) * hue/60 + elseif hue < 180 then + return n2 + elseif hue < 240 then + return n1 + (n2 - n1) * (240 - hue)/60 + else + return n1 + end + end + local m2 + if l < 0.5 then + m2 = l * (1 + s) + else + m2 = l + s - l * s + end + local m1 = 2 * l - m2 + if s == 0 then + -- Achromatic, there is no hue + -- In book this errors if hue is not undefined, but we set hue to 0 in this case, not nil or something, so + return color(l, l, l, alpha) + else + -- Chromatic case, so there is a hue + return color( + clip(value(m1, m2, h + 120)), + clip(value(m1, m2, h)), + clip(value(m1, m2, h - 120)), + alpha + ) + end +end + +color = lib.class { + __tostring = function(self) + local hex = function(val) + return string.format('%02X',math.max(0,math.min(0xff,math.floor(0xff*val)))) + end + local str = '#' .. + hex(self.red) .. + hex(self.green) .. + hex(self.blue) + if self.alpha and self.alpha < 1.0 then str = str .. hex(self.alpha) end + return str + end; + + __add = function(self, other) + local sfac = (self.alpha or 1.0) / 1.0 + local ofac = (other.alpha or 1.0) / 1.0 + if self.alpha == other.alpha then + sfac = 1 ofac = 1 + end + + local sr, sg, sb = other.red * ofac, other.blue * ofac, other.green * ofac + local nr, ng, nb = self.red * sfac, self.blue * sfac, self.green * sfac + local saturate = function(a,b) + return math.max(0, math.min(1.0, a+b)) + end + local alpha = nil + if self.alpha and other.alpha then + alpha = saturate(self.alpha or 1.0, other.alpha or 1.0) + end + return color( + saturate(sr, nr), + saturate(sg, ng), + saturate(sb, nb), + alpha + ) + end; + + cast = { + number = function(n) return { + red = n; green = n; blue = n; + } end; + table = function(t) return { + red = t[1]; green = t[2]; blue = t[3]; alpha = t[4]; + } end; + }; + + __index = { + hex = function(self) return + getmetatable(self).__tostring(self) + end; + + int24 = function(self) + return bit.bor( + bit.lshift(math.floor(0xff*self.red), 16), + bit.lshift(math.floor(0xff*self.green), 8), + math.floor(0xff*self.blue)) + end; + + int = function(self) + return bit.bor(bit.lshift(self:int24(), 8), math.floor(0xff*(self.alpha or 1.0))) + end; + + fmt = function(self, text) return + minetest.colorize(self:hex(), text) + end; + + bg = function(self, text) return + text .. minetest.get_background_escape_sequence(self:hex()) + end; + + lum = function(self) return + (self.red + self.green + self.blue) / 3 + end; + + pair = function(self) --> bg, fg + if self:to_hsl().lum > 0.5 then -- dark on light + return self:brighten(1.2), self:brighten(0.1) + else -- light on dark + return self:brighten(0.6), self:readable(.9, 1.0) + end + end; + + marshal = function(self) + local raw = colorStruct.enc { + red = math.floor(self.red * 0xff); + green = math.floor(self.green * 0xff); + blue = math.floor(self.blue * 0xff); + alpha = math.floor(self.alpha * 0xff); + } + return lib.str.meta_armor(raw) + end; + to_hsl_o = function(self) + local s = self:to_hsl() + return { + hue = s.hue; + sat = s.sat*2-1; + lum = s.lum*2-1; + } + end; + + to_hsl = function(self) + -- THIS DOESN'T WORK. color(hsl):to_hsl() ~= hsl. + -- has ugly implications for light control + + -- Based on the algorithm in Computer Graphics: Principles and Practice, by + -- James D. Foley et. al., 2nd ed., p. 595 + -- We need the rgb between 0 and 1 + local r = self.red + local g = self.green + local b = self.blue + local max = math.max(r, g, b) + local min = math.min(r, g, b) + local luminosity = (max + min)/2 + local hue = 0 + local saturation = 0 + if max == min then + -- Achromatic case, because r=g=b + saturation = 0 + hue = 0 -- Undefined, so just replace w/ 0 for usability + else + -- Chromatic case + if luminosity <= 0.5 then + saturation = (max - min)/(max + min) + else + saturation = (max - min)/(2 - max - min) + end + -- Next calculate the hue + local delta = max - min + if r == max then + hue = (g - b)/delta + elseif g == max then + hue = 2 + (b - r)/delta + else -- blue must be max, so no point in checking + hue = 4 + (r - g)/delta + end + hue = hue * 60 -- degrees + --hue = hue * (math.pi / 3) -- for hue in radians instead of degrees + if hue < 0 then + hue = hue + 2 * math.pi + end + end + -- print("r"..self.red.."g"..self.green.."b"..self.blue.." is h"..hue.."s"..saturation.."l"..luminosity) + --local temp = from_hsl({hue=hue,sat=saturation,lum=luminosity},self.alpha) + -- print("back is r"..temp.red.."g"..temp.green.."b"..temp.blue) + if hue < 0 then + hue = 360 + hue + end + return { hue = hue, sat = saturation, lum = luminosity, alpha = self.alpha } + end; + + readable = function(self, target, minalpha) + target = target or 0.6 + local hsl = self:to_hsl() + hsl.lum = target + local worstHue = 230 + local nearness = math.abs(worstHue - hsl.hue) + if nearness <= 70 then + local boost = 1.0 - (nearness / 70) + hsl.lum = math.min(1, hsl.lum * (1 + (boost*0.4))) + end + + return from_hsl(hsl, math.max(self.alpha or 1.0, minalpha)) + end; + + fade = warp(function(new, fac) + new.alpha = math.min(1.0, (new.alpha or 1.0) * fac) + end); + + warp = warp(function(new, fn) fn(new) end); + + lerp = warp(function(self, new, fac) -- uses rgb color space + local function lerp(t, a, b) return (1-t)*a + t*b end + self.red = lerp(fac, self.red, new.red) + self.green = lerp(fac, self.green, new.green) + self.blue = lerp(fac, self.blue, new.blue) + if self.alpha ~= nil or new.alpha ~= nil then + if new.alpha == nil and fac >= 1.0 then + self.alpha = nil + elseif self.alpha == nil and fac <= 0.0 then + self.alpha = nil + else + self.alpha = lerp(fac, self.alpha or 1.0, new.alpha or 1.0) + end + end + end); + + brighten = function(self, fac) + -- Use HSL to brighten + -- To HSL + local hsl = self:to_hsl() + -- Do the calculation, clamp to 0-1 instead of the clamp fn + hsl.lum = math.min(math.max(hsl.lum * fac, 0), 1) + -- Turn back into RGB color + -- local t = from_hsl(hsl, self.alpha) + -- print("darker is r"..hsl.red.."g"..hsl.green.."b"..hsl.blue) + -- print("brighten is r"..t.red.."g"..t.green.."b"..t.blue) + return from_hsl(hsl, self.alpha) + end; + + darken = warp(function(new, fac) + -- TODO: is there any point to this being different than brighten? Probably especially not now. + new.red = clip(new.red - (new.red * fac)) + new.blue = clip(new.blue - (new.blue * fac)) + new.green = clip(new.green - (new.green * fac)) + end); + }; + + construct = function(r,g,b,a) + local new = {} + if g == nil then + if type(r) == 'string' then + assert(false) -- TODO parse color string + elseif type(r) == 'table' then + if r.hue then + return from_hsl(r, r.alpha or g) + elseif r.r and r.g and r.b then + new.red = r.r + new.green = r.g + new.blue = r.b + new.alpha = r.a + else + new.red = r[1] + new.green = r[2] + new.blue = r[3] + new.alpha = r[4] + end + else assert(false) end + else + if color.id(r) then + new.red = r.red + new.green = r.green + new.blue = r.blue + new.alpha = g + else + new.red = r + new.green = g + new.blue = b + new.alpha = a + end + end + return new + end +} + +function color.unmarshal(str) + local raw = lib.str.meta_dearmor(raw) + local o = colorStruct.dec(raw) + return color( + o.red / 0xff, + o.green / 0xff, + o.blue / 0xff, + o.alpha / 0xff + ) +end; + +return color ADDED mods/vtlib/dbg.lua Index: mods/vtlib/dbg.lua ================================================================== --- mods/vtlib/dbg.lua +++ mods/vtlib/dbg.lua @@ -0,0 +1,53 @@ +local lib = ... +local dbg = { + aloud = false; +} + +local lastmod, lastarea + +function dbg.debugger(area) + local depth = 0 + local d = {} + function d.enter() depth = depth+1 end + function d.exit() depth = depth-1 end + local mod = minetest.get_current_modname() + if dbg.aloud then + function d.report(fmt, ...) + local where = debug.getinfo(2) + local caller = debug.getinfo(3) + if mod and (lastmod ~= mod or lastarea ~= area) then + local ms = mod or '' + if area then ms = ms .. '.' .. area end + print(string.format('\27[1mmodule \27[31m%s\27[m\n%s', ms, string.rep('-', #ms + 7))) + lastmod, lastarea = mod, area + end + local callsource = string.format('%s:%s', caller.name, caller.currentline) + if caller.source ~= where.source then + callsource = callsource .. caller.source + end + print( + string.rep(' ', depth) .. + string.format( + '> \27[1m%s:%s\27[m ← \27[34m%s\27[m :: %s', + where.name, where.currentline, + callsource, + string.format(fmt, ...) + ) + ) + end + function d.wrap(fn) + return function(...) + d.enter() + local ret = {fn(...)} + d.exit() + return (table.unpack or unpack)(ret) + end + end + else + function d.report() end + function d.wrap(fn) return fn end + end + return d +end + +return dbg ADDED mods/vtlib/image.lua Index: mods/vtlib/image.lua ================================================================== --- mods/vtlib/image.lua +++ mods/vtlib/image.lua @@ -0,0 +1,134 @@ +local lib = ... + +local image +image = lib.class { + + __concat = function(self,with) return self:blit(with) end; + __tostring = function(self) return self:render() end; + __index = { + render = function(self) + local str = '' + local bracket = false + if self.combine then + str = string.format('[combine:%sx%s', self.w, self.h) + for _,i in pairs(self.atop) do + str = str .. string.format(':%s,%s=(%s)', i.at.x, i.at.y, i.img:render()) + end + else + for _,i in pairs(self.atop) do + str = '(' .. i.img:render() .. ')^' .. str + end + if str ~= '' then + str = str .. '(' + bracket = true + end + str = str .. self.string + end + for _,e in pairs(self.fx) do + str = str .. '^[' .. e + -- be sure to escape ones that take arguments + -- correctly! + end + if bracket then str = str .. ')' end + return str + end; + + -- must be used to mark an image before + -- the second form of blit can be used + compound = function(self, w, h) + return image.change(self, { + combine = true; + w = w, h = h; + }) + end; + + blit = function(self, img, at) + assert((not at) or self.combine) + if img then return image.change(self, { + atop = lib.tbl.append(self.atop, {{img=img, at=at}}) + }) else return self end + end; + + multiply = function(self, color) + return image.change(self, { + fx = lib.tbl.append(self.fx, {'multiply:' .. tostring(color)}) + }) + end; + + paint = function(self, color, ratio) + return image.change(self, { + fx = lib.tbl.append(self.fx, {'colorize:' .. tostring(color) .. ':' .. ratio}) + }) + end; + + tint = function(self, color) + if not color.hue then + if not lib.color.id(color) then + color = lib.color(color) + end + color = color:to_hsl() + end + return image.change(self, { + fx = lib.tbl.append(self.fx, { + string.format('colorizehsl:%s:%s:%s', + color.hue, color.sat*100, color.lum*100) + }) + }) + end; + + shift = function(self, color) + if color.hue == nil or color.sat == nil or color.lum == nil then + if not lib.color.id(color) then + color = lib.color(color) + end + color = color:to_hsl() + end + return image.change(self, { + fx = lib.tbl.append(self.fx, { + string.format('hsl:%s:%s:%s', + color.hue, color.sat*100, color.lum*100) + }) + }) + end; + + rehue = function(self, hue) + return self.shift{hue=hue, sat=0, lum=0} + end; + + colorize = function(self, color) + local hsl + if color.hue then + hsl = color + else + hsl = color:to_hsl() + end + return self:shift { + hue = hsl.hue; + sat = hsl.sat * 2 - 1; + lum = hsl.lum * 2 - 1; + } + end; + + fade = function(self, fac) + return image.change(self, { + fx = lib.tbl.append(self.fx, {'opacity:' .. (255 - 255*fac)}) + }) + end; + + transform = function(self, kind) + return image.change(self, { + fx = lib.tbl.append(self.fx, {'transform' .. tostring(kind)}) + }) + end; + + glow = function(self,color) return self:blit(self:multiply(color)) end; + }; + + construct = function(file, w, h) return { + string = file; + atop = {}; + fx = {}; + combine = w and h and true or nil; + } end; +} +return image ADDED mods/vtlib/init.lua Index: mods/vtlib/init.lua ================================================================== --- mods/vtlib/init.lua +++ mods/vtlib/init.lua @@ -0,0 +1,39 @@ +local ident = minetest.get_current_modname() +local path = minetest.get_modpath(ident) + +local lib = {} +_G[ident] = lib + +local function +component(name) + local p = string.format('%s/%s.lua', path, name) + print('[vtlib] loading component ' .. p) + local chunk, err = loadfile(p) + if chunk == nil then error(err) end + lib[name] = chunk(lib, ident, path) +end + +component 'dbg' + +-- primitive manip +component 'tbl' +component 'class' +component 'math' +component 'str' + +-- reading and writing data formats +component 'marshal' + +-- classes +component 'color' +component 'image' +component 'ui' +component 'tree' + +-- organization +component 'registry' + +-- game object manip +component 'item' +component 'node' +component 'obj' ADDED mods/vtlib/item.lua Index: mods/vtlib/item.lua ================================================================== --- mods/vtlib/item.lua +++ mods/vtlib/item.lua @@ -0,0 +1,90 @@ +local fn = {} +local lib = ... + +fn.match = function(a,b,exact) + if exact == nil then exact = true end + if exact then return ItemStack(a):to_string() == ItemStack(b):to_string() else + a,b = ItemStack(a), ItemStack(b) + if a:get_name() ~= b:get_name() then return false, b end + if a:get_count() <= b:get_count() then + return true, ItemStack { + name = a:get_name(); + count = b:get_count() - a:get_count(); + } + end + end +end + +-- it is extremely unfortunate this function needs to exist. +-- minetest needs to export its matching capabilities already +fn.groupmatch = function(identity,item,exact) + if exact == nil then exact = true end + local count + if type(identity) == 'table' then + count = identity.count + identity = identity.name + else + if lib.str.beginswith(identity, 'group:') then + identity,count = lib.tbl.split(identity,'%s+',true) + if count + then count = tonumber(count) + else count = 1 + end + else + local is = ItemStack(identity) + identity,count = is:get_name(), is:get_count() + end + end + + if lib.str.beginswith(identity, 'group:') then + local stack = ItemStack(item) + local groups = lib.str.explode(string.sub(identity,7), ',') + for _,g in pairs(groups) do + local rn,rv = lib.tbl.split(g,'=') + local gv = minetest.get_item_group(stack:get_name(), rn) + if rv then + if gv ~= tonumber(rv) then return false, stack end + else + if (not gv) or gv == 0 then return false, stack end + end + end + + if stack:get_count() < count then return false, stack end + + if exact then + if stack:get_count() ~= count then + return false, stack end + return true, ItemStack(nil) + else + stack:take_item(count) + return true, stack + end + else return fn.match(identity,item,exact) end +end + +-- local try = function(a,b,e) +-- print('::: match',e==false and 'inexact' or 'exact',dump(a)) +-- print('\t against',dump(b)) +-- local res, leftover = fn.groupmatch(a,b,e) +-- print('result',res) +-- if leftover and leftover:get_count() > 0 then +-- print('leftover items:',leftover:to_string()) +-- end +-- print("\n") +-- end + +function fn.getStorage(item, var) + return lib.marshal.t.inventoryList.dec( + lib.str.meta_dearmor( + item:get_meta():get_string(var) + ) + ) +end + +function fn.setStorage(item, var, lst) + item:get_meta():set_string(var, lib.str.meta_armor( + lib.marshal.t.inventoryList.enc(lst) + )) +end + +return fn ADDED mods/vtlib/marshal.lua Index: mods/vtlib/marshal.lua ================================================================== --- mods/vtlib/marshal.lua +++ mods/vtlib/marshal.lua @@ -0,0 +1,461 @@ +-- [ʞ] marshal.lua +-- ~ lexi hale +-- © EUPLv1.2 +-- ? a replacement for the shitty old Sorcery marshaller +-- this marshaller focuses on reliability and extensibility; +-- it is much less space-efficient than the (broken) Sorcery +-- marshaller or the slick parvan marshaller. i don't care anymore + +local lib = ... +local m = { + t = {}; + g = {}; +} + +local T,G = m.t, m.g + +-- a type is an object with two functions 'enc' and 'dec' + +---------------- +--- utilities -- +---------------- + + +local debugger = lib.dbg.debugger 'marshal' +local report = debugger.report + +function m.streamReader(blob) + local idx = 1 + local blobLen = #blob + local function advance(ct) + if not ct then + --report('advancing to end of %s-byte blob', blobLen) + idx = blobLen+1 + else + --report('advancing %s bytes from %s of %s', ct, idx, blobLen) + assert(idx+ct <= blobLen + 1) + idx = idx + ct + end + end + local function dataLeft() return idx <= blobLen end + local function consume(ct) + if ct == 0 then return '' end + assert(dataLeft(), string.format('wanted %s bytes but no data left: %s/%s', ct, idx, blobLen)) + local str = string.sub(blob, idx, + ct and idx + ct - 1 or nil) + advance(ct) + return str + end + return { + dataLeft = dataLeft; + advance = advance; + consume = consume; + dec = function(t) -- parse a fixed-size type in the stream + assert(t.sz, 'type ' .. t.name .. ' is variably-sized') + report('parsing type %s from stream at %s/%s', t.name, idx, blobLen) + return t.dec(consume(t.sz)) + end; + } +end + +function m.streamEncoder(sizeType) + local encFrags = {} + local encoder = {} + function encoder.push(v, ...) + assert(type(v) == 'string') + if v ~= nil then + table.insert(encFrags, v) + end + if select('#', ...) > 0 then encoder.push(...) end + end + function encoder.ppush(...) -- "pascal push" + local sz = 0 + local function szi(e, ...) + if e then sz = sz + #e end + if select('#') > 0 then szi(...) end + end + szi(...) + encoder.push(sizeType.enc(sz), ...) + end + function encoder.pspush(v, ...) -- "pascal struct push" + if v~=nil then encoder.ppush(v) end + if select('#', ...) > 0 then encoder.prpush(...) end + end + function encoder.peek() + return table.concat(encFrags) + end + function encoder.pull() + local s = encoder.peek() + encFrags = {} + return s + end + + return encoder +end + +function m.metaStore(map, prefix) + report('generating metaStore for %s', dump(map)) + if prefix == true then prefix = minetest.get_current_modname() end + local function keyFor(k) + k = map[k].key + if prefix then return prefix .. ':' .. k end + return k + end + + return function(obj) + local m = obj:get_meta() + local store = {} + function store.write(key, val) + report('store: setting %q(%q)=%s (mapping %s)', key, keyFor(key), dump(val), dump(map[key])) + local armored = lib.str.meta_armor(map[key].type.enc(val)) + m:set_string(keyFor(key), armored) + return store + end + function store.read(key) + report('store: reading %q', key) + local dearmored = lib.str.meta_dearmor(m:get_string(keyFor(key))) + return map[key].type.dec(dearmored) + end + function store.erase(key) + m:set_string(keyFor(key), '') + return store + end + function store.over(key,fn) + local n = fn(read(key)) + if n ~= nil then write(key,n) end + return store + end + return store + end +end + + +------------------------------- +-- generic type constructors -- +------------------------------- + +function G.int(bits,signed) + local bytes = math.ceil(bits / 8) + local max = 2 ^ bits + local spoint = math.floor(max/2) + return { + sz = bytes; + name = string.format("%sint<%s>", + signed and 's' or 'u', bits + ); + enc = function(obj) + obj = obj or 0 + local val = math.abs(obj) + local str = '' + if signed then + local max = math.floor(max / 2) + if (obj > max) or (obj < (0-(max+1))) then + return m.err.domain end + if obj < 0 then val = val + spoint end + -- e.g. for 8bit: 0x80 == -1; 0xFF = -128 + else + if val > max then return m.err.domain end + end + for i=1,bytes do + local n = math.fmod(val, 0x100) + str = str .. string.char(n) + val = math.floor(val / 0x100) + end + return str + end; + dec = function(str) + local val = 0 + for i = 0, bytes-1 do + local b = string.byte(str,bytes - i) + val = (val * 0x100) + (b or 0) + end + if signed then + if val > spoint then val = 0 - (val - spoint) end + end + return val + end; + } +end + +local size = G.int(32, false) + + +function G.struct(...) + -- struct record { + -- uint< 8> keySz; + -- uint<32> valSz; + -- string[keySz] name; + -- string[valSz] data; + -- } + -- struct struct { + -- uint<32> nRecords; + -- record[nRecords] records; + -- } + local def, name + if select('#', ...) >= 2 then + name, def = ... + else + def = ... + end + name = 'struct' .. (name and ':' .. name or ''); + report('defining struct name=%q fields=%s', name, dump(def)) + return { + name = name; + enc = function(obj) + local enc = m.streamEncoder() + local n = 0 + for k,ty in pairs(def) do n=n+1 + local encoded = ty.enc(obj[k]) + enc.push(T.u8.enc(#k), size.enc(#encoded), k, encoded) + end + return size.enc(n) .. enc.peek() + end; + dec = debugger.wrap(function(blob) + if blob == '' then + -- struct is more likely to be used directly, as the top of a serialization + -- tree, which means it is more likely to be exposed to ill-formed input. + -- a particularly common case will be the empty string, returned when a + -- get_string is performed on an empty key. i think the most sensible behavior + -- here is to return nil, rather than just crashing + return nil + end + local s = m.streamReader(blob) + local obj = {} + report('struct.dec: decoding type %s; reading string %s', name, dump(blob)) + local nRecords = s.dec(size) + while s.dataLeft() and nRecords > 0 do + report('%s records left', nRecords) + local ksz = s.dec(T.u8) + local vsz = s.dec(size) + local k = s.consume(ksz) + local v = s.consume(vsz) + local ty = def[k] + report('decoding field %s of type %s, %s bytes', k, ty.name, vsz) + if not ty then + report('warning: unfamiliar record %q found in struct', k) + else + obj[k] = ty.dec(v) + end + nRecords = nRecords - 1 + end + if s.dataLeft() then + report('warning: junk at end of struct %q',s.consume()) + end + report('returning object %s', dump(obj)) + return obj + end); + } +end + +function G.fixed(bits, base, prec, sign) + local c = G.int(bits, sign) + local mul = base ^ prec + return { + sz = c.sz; + name = string.format("%sfixed<%s,%s,%s>", + sign and 's' or 'u', + bits, base, prec + ); + enc = function(v) + return c.enc(v) + end; + dec = function(s) + local v = c.dec(s) + return v / mul + end; + } +end + +function G.range(min, max, bits) + local d = max-min + local precType = G.fixed(bits, d, 1, false) + return { + sz = precType.sz; + name = string.format("range<%s,%s~%s>", + bits, min, max + ); + enc = function(v) + return precType.enc((v - min) / d) + end; + dec = function(s) + local v = precType.dec(s) + return d*v + min + end; + } +end + +T.str = { + name = 'str'; + enc = function(s) return s end; + dec = function(s) return s end; +} + + +function G.array(bitlen,t) + local sz = G.int(bitlen,false) + local name = string.format("array<%s,%s>", + sz.name, t.name + ); + return { + name = name; + enc = debugger.wrap(function(obj) + local s = m.streamEncoder(size) + report('encoding array of type %s', name) + local nVals = (obj and #obj) or 0 + s.push(sz.enc(nVals)) + for i=1,nVals do + report('encoding value %s: %s', i, dump(obj[i])) + s.ppush(t.enc(obj[i]) or '') + end + report('returning blob %q', s.peek()) + return s.peek() + end); + dec = debugger.wrap(function(blob) + local s = m.streamReader(blob) + local ct = s.dec(sz) + local obj = {} + report('decoding array %s of size %s', name, ct) + for i=1,ct do + report('decoding elt [%s] of %s', i, ct) + local eltsz = s.dec(size) + obj[i] = t.dec(s.consume(eltsz)) + end + if s.dataLeft() then + print('warning: junk at end of array', dump(s.consume)) + end + return obj + end); + } +end + +function G.enum(values) + local n = #values + local bits = 8 + if n > 65536 then bits = 32 -- don't think we really need any more + elseif n > 256 then bits = 16 end + local t = G.int(bits, false) + local map = {} + for k,v in pairs(values) do map[v] = k end + return { + name = string.format("enum<[%s]>", dump(values) ); + sz = t.sz; + enc = function(v) + local iv = map[v] or error('value ' .. v .. ' not allowed in enum') + return t.enc(iv) + end; + dec = function(s) + return values[t.dec(s)] + end; + } +end + +function G.class(struct, extract, construct) + return { + sz = struct.sz; + name = string.format("class<%s>", + struct.name + ); + enc = debugger.wrap(function(v) + report('encoding class<%s>', struct.name) + return struct.enc(extract(v)) + end); + dec = debugger.wrap(function(s) + report('decoding class<%s>', struct.name) + return construct(s ~= '' and struct.dec(s) or nil) + -- allow classes to handle empty metastrings after their own fashion + end); + } +end + + + +------------------------- +-- common type aliases -- +------------------------- + +for _, sz in pairs{8,16,32,64} do + T['u' .. tostring(sz)] = G.int(sz,false) + T['s' .. tostring(sz)] = G.int(sz,true) +end + + +T.factor = G.fixed(16, 10.0, 3, false); +T.decimal = G.fixed(32, 10.0, 5, true); +T.fixed = G.fixed(32, 2.0, 16, true); +T.double = G.fixed(64, 10.0, 10, true); +T.wide = G.fixed(64, 2.0, 32, true); + +T.angle = G.range( 0, 360, 16); +T.turn = G.range(-360, 360, 16); +T.clamp = G.range( 0, 1.0, 16); +T.tinyClamp = G.range( 0, 1.0, 8); + +----------------------------------- +-- abstractions over engine data -- +----------------------------------- + +T.inventoryList = G.class( + G.array(16, G.struct ('inventoryItem', { + index = T.u16; + itemString = T.str; + })), + function(lst) + report('encoding inventory list %s', dump(lst)) + local ary = {} + for i, s in pairs(lst) do + if not s:is_empty() then + table.insert(ary, {index = i, itemString = s:to_string()}) + end + end + report('list structure: %s', dump(ary)) + return ary + end, + function(ary) + report('decoding inventory list %s', dump(ary)) + if not ary then return {} end + local tbl = {} + for _, s in pairs(ary) do + tbl[s.index] = ItemStack(s.itemString) + end + return tbl + end +); + +T.inventory = G.class( + G.array(8, G.struct('inventory', { + name = T.str; + items = T.inventoryList; + })), + function (inv) + if inv.get_lists then + inv = inv:get_lists() + end + local lst = {} + for name, items in pairs(inv) do table.insert(lst, {name=name,items=items}) end + return lst + end, + function (tbl) + if not tbl then return {} end + local inv = {} + for _, e in pairs(tbl) do + inv[e.name] = e.items + end + return inv + end +); + +------------------- +-- legacy compat -- +------------------- + +-- strings are now fixed at 32-bit sizetype +T.text = T.str +T.phrase = T.str +function G.blob() return T.str end + +function m.transcoder(tbl) + local ty = G.struct(tbl) + return ty.enc, ty.dec +end + +return m ADDED mods/vtlib/math.lua Index: mods/vtlib/math.lua ================================================================== --- mods/vtlib/math.lua +++ mods/vtlib/math.lua @@ -0,0 +1,132 @@ +local lib = ... +local fn = {} + +fn.vsep = function(vec) -- separate a vector into a direction + magnitude + return vec:normalize(), vec:length() +end + +-- minetest now only provides the version of this function that sqrts the result +-- which is pointlessly wasteful much of the time +fn.vdsq = function(a,b) + local d = vector.subtract(v1,v2) + return (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2) +end + +fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points + -- (cheaper than calculating distance outright) + local d if v2 + then d = vector.subtract(v1,v2) + else d = v1 + end + local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2) + return dsq / (dist^2) + -- [0,1) == less then + -- 1 == equal + -- >1 == greater than +end + +-- produce an SI expression for a quantity +fn.si = function(unit, val, full, uncommonScales) + if val == 0 then return '0 ' .. unit end + local scales = { + {30, 'Q', 'quetta',true, 'q', 'quecto',true}; + {27, 'R', 'ronna', true, 'r', 'ronto', true}; + {24, 'Y', 'yotta', true, 'y', 'yocto', true}; + {21, 'Z', 'zetta', true, 'z', 'zepto', true}; + {18, 'E', 'exa', true, 'a', 'atto', true}; + {15, 'P', 'peta', true, 'f', 'femto', true}; + {12, 'T', 'tera', true, 'p', 'pico', true}; + {9, 'G', 'giga', true, 'n', 'nano', true}; + {6, 'M', 'mega', true, 'μ', 'micro', true}; + {3, 'k', 'kilo', true, 'm', 'milli', true}; + {2, 'h', 'hecto', false, 'c', 'centi', true}; + {1, 'da','deca', false, 'd', 'deci', false}; + } + for i, s in ipairs(scales) do + local amt, smaj, pmaj, cmaj, + smin, pmin, cmin = lib.tbl.unpack(s) + + if math.abs(val) > 1 then + if uncommonScales or cmaj then + local denom = 10^amt + if math.abs(val) >= (10^(amt)) then + return string.format("%s %s%s", + val / denom, (full and pmaj or smaj), unit) + end + end + elseif math.abs(val) < 1 then + if uncommonScales or cmin then + local denom = 10^-amt + if math.abs(val) <= (10^-(amt-1)) then + return string.format("%s %s%s", + val / denom, (full and pmin or smin), unit) + end + end + end + end + + return string.format("%s %s", val, unit) +end + +function fn.lerp(t, a, b) return (1-t)*a + t*b end + +function fn.trim(fl, prec) + local fac = 10^prec + return math.floor(fl * fac) / fac +end + +function fn.sign(v) + if v > 0 then return 1 + elseif v < 0 then return -1 + else return 0 end +end + +function fn.toward(from, to, mag) + local dir = fn.sign(to - from) + local step = mag * dir + if (dir == 1 and from+step < to) + or (dir == -1 and from+step > to) + then return from+step + else return to + end +end + +fn.rng = lib.class { + __name = 'rng'; + construct = function(seed) + return {seed = seed, rng = PcgRandom(seed)} + end; + __index = { + int = function(self,m,x) + return self.rng:next(m,x) + end; + real = function(self,m,x) + local i = self:int() + local f = (i+bit.lshift(1,31)) / bit.lshift(1, 32) + if m==nil then return f end + if x==nil then x=m m=0 end + return m + ((x-m) * f) + end; + fork = function(self) + return fn.rng(self:int()) + end; + }; + __add = function(self,n) + return fn.rng(self.seed + n) + end; +} + +fn.seedbank = lib.class { + __name = 'seedbank'; + construct = function(seed) + return {seed = seed} + end; + __index = function(self, n) + return fn.rng(PcgRandom(self.seed+n):next()) + end; + __add = function(self, n) + return fn.seedbank(self.seed + n) + end; +} +-- function fn.vlerp +return fn ADDED mods/vtlib/mod.conf Index: mods/vtlib/mod.conf ================================================================== --- mods/vtlib/mod.conf +++ mods/vtlib/mod.conf @@ -0,0 +1,4 @@ +name = vtlib +description = velartrill's utility library +author = velartrill +title = ʞlib ADDED mods/vtlib/node.lua Index: mods/vtlib/node.lua ================================================================== --- mods/vtlib/node.lua +++ mods/vtlib/node.lua @@ -0,0 +1,310 @@ +local lib = ... +local V = vector.new +local ofs = { + neighbors = { + V( 0, 1, 0); + V( 0, -1, 0); + V( 1, 0, 0); + V(-1, 0, 0); + V( 0, 0, 1); + V( 0, 0, -1); + }; + corners = { + V( 1, 0, 1); + V(-1, 0, 1); + V(-1, 0, -1); + V( 1, 0, -1); + }; + planecorners = { + V( 1, 0, 1); + V(-1, 0, 1); + V(-1, 0, -1); + V( 1, 0, -1); + + V( 1, 1, 0); + V(-1, 1, 0); + V(-1, -1, 0); + V( 1, -1, 0); + }; + cubecorners = { + V( 1, 1, 1); + V(-1, 1, 1); + V(-1, -1, 1); + V(-1, -1, -1); + V( 1, -1, -1); + V( 1, 1, -1); + V( 1, -1, 1); + V(-1, 1, -1); + }; + nextto = { + V( 1, 0, 0); + V(-1, 0, 0); + V( 0, 0, 1); + V( 0, 0, -1); + }; + cardinal = { + V( 1, 0, 0); + V(-1, 0, 0); + V( 0, 0, 1); + V( 0, 0, -1); + }; +} + +ofs.adjoining = lib.tbl.append(lib.tbl.append( + ofs.neighbors,ofs.planecorners),ofs.cubecorners) + +local purge_container = function(only, pos,node,meta,user) + local offset = function(pos,range) + local r = function(min,max) + return (math.random() * (max - min)) + min + end + return { + x = pos.x + r(0 - range, range); + y = pos.y; + z = pos.z + r(0 - range, range); + } + end + for name, inv in pairs(meta.inventory) do + if only and not lib.tbl.has(only,name) then goto skip end + for _, item in pairs(inv) do + if not item:is_empty() then + minetest.add_item(offset(pos,0.4), item) + end + end + ::skip::end +end; + +local force = function(pos,preload_for) + local n = minetest.get_node_or_nil(pos) + if preload_for then lib.node.preload(pos,preload_for) end + if n then return n end + + minetest.load_area(pos) + return minetest.get_node(pos) +end; + +local amass = function(startpoint,names,directions) + if not directions then directions = ofs.neighbors end + local check = function(n) + return lib.tbl.has(names, n.name, function(check,against) + return lib.item.groupmatch(against,check) + end)-- match found + end + if type(names) == 'function' then check = names end + local nodes, positions, checked = {},{},{} + local checkedp = function(pos) + for _,v in pairs(checked) do + if vector.equals(pos,v) then return true end + end + return false + end + local i,stack = 1,{startpoint} repeat + local pos = stack[i] + local n = force(pos) + if check(n, pos, nodes, positions) then + -- record the find + nodes[pos] = n.name + if positions[n.name] + then positions[n.name][#positions[n.name]+1] = pos + else positions[n.name] = {pos} + end + + -- check selected neighbors to see if any need scanning + for _,d in pairs(directions) do + local sum = vector.add(pos, d) + if not checkedp(sum) then + stack[#stack + 1] = sum + checked[#checked+1] = sum + end + end + end + checked[#checked+1] = pos + i = i + 1 + until i > #stack + return nodes, positions +end; + +local is_air = function(pos) + local n = force(pos) + if n.name == 'air' then return true end + local d = minetest.registered_nodes[n.name] + if not d then return false end + return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true) +end; + +local is_clear = function(pos) + if not lib.node.is_air(pos) then return false end + local ents = minetest.get_objects_inside_radius(pos,0.5) + if #ents > 0 then return false end + return true +end; + +local function boxwarp(nb, mogrifier, par) + if nb == nil then + return + elseif nb.type then + if nb.type == 'fixed' or nb.type == 'leveled' then + for i, b in ipairs(nb.fixed) do + boxwarp(b, mogrifier, nb) + end + elseif nb.type == 'wallmounted' then + boxwarp(nb.wall_top, mogrifier, nb) + boxwarp(nb.wall_bottom, mogrifier, nb) + boxwarp(nb.wall_side, mogrifier, nb) + elseif nb.type == 'connected' then + for _, state in pairs{'connect', 'disconnected'} do + for _, dir in pairs{'top','bottom','front','left','back','right'} do + boxwarp(nb[state ..'_'.. dir], mogrifier, nb) + end + boxwarp(nb.disconnected, mogrifier, nb) + boxwarp(nb.disconnected_sides, mogrifier, nb) + end + elseif nb.type == 'regular' then + nb.type = 'fixed' + nb.fixed = {-.5, -.5, -.5; .5, .5, .5}; + boxwarp(nb.fixed, mogrifier, nb); + end + elseif nb[1] then + mogrifier(nb, par) + end +end + +local function boxwarped(box, warp) --oof + local c = lib.tbl.deepcopy(box) + boxwarp(c, warp) + return c +end + +return { + offsets = ofs; + purge_container = function(...) return purge_container(nil, ...) end; + purge_only = function(lst) + return function(...) + return purge_container(lst, ...) + end + end; + + is_air = is_air; + is_clear = is_clear; + + insert = function(item, slot, npos, user, inv) + inv = inv or minetest.get_meta(npos):get_inventory() + if inv:room_for_item(slot,item) then + inv:add_item(slot,item) + else repeat + if user then + local ui = user:get_inventory() + if ui:room_for_item('main', item) then + ui:add_item('main', item) + break + end + end + minetest.add_item(npos, item) + until true end + end; + + install_bed = function(bed, where, dir) + local bottom = bed .. '_bottom' + local top = bed .. '_top' + local d + if type(dir) == 'number' then + d = dir + dir = minetest.facedir_to_dir(d) + else + d = minetest.dir_to_facedir(dir) + end + if not is_clear(where) and is_clear(where - dir) then return false end + minetest.set_node(where, {name = top, param2 = d}) + minetest.set_node(where - dir, {name = bottom, param2 = d}) + return true + end; + + get_arrival_point = function(pos) + local try = function(p) + local air = lib.node.is_clear + if air(p) then + if air(vector.offset(p,0,1,0)) then return p end + if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end + end + return false + end + + do local t = try(pos) if t then return t end end + for _,o in pairs(ofs.neighbors) do + local p = vector.add(pos, o) + do local t = try(p) if t then return t end end + end + end; + + + forneighbor = function(pos, n, fn) + for _,p in pairs(n) do + local sum = vector.add(pos, p) + local n = minetest.get_node(sum) + if n.name == 'ignore' then + minetest.load_area(sum) + n = minetest.get_node(sum) + end + if fn(sum, n) == false then break end + end + end; + + amass = amass; + + force = force; + + blockpos = function(pos) + return { + x = math.floor(pos.x / 16); + y = math.floor(pos.y / 16); + z = math.floor(pos.z / 16); + } + end; + + preload = function(pos, user) + minetest.load_area(pos) + user:send_mapblock(lib.node.blockpos(pos)) + end; + + discharger = function(pos) + local below = force(vector.subtract(pos,{x=0,y=1,z=0})) + if below.name == 'hopper:hopper' + or below.name == 'hopper:hopper_side' then + local hopper = minetest.get_meta(below):get_inventory() + return function(i) + if hopper:room_for_item('main',i) then + return hopper:add_item('main',i), true + end + return i, false + end + else + return function(i) return i, false end + end + end; + + autopreserve = function(id, tbl) + tbl.drop = tbl.drop or { + max_items = 1; + items = { + { items = {id} }; + }; + } + local next_apn = tbl.after_place_node + tbl.after_place_node = function(...) local pos, who, stack = ... + minetest.get_meta(pos):from_table(stack:get_meta():to_table()) + if next_apn then return next_apn(...) end + end + local next_pm = tbl.preserve_metadata + tbl.preserve_metadata = function(...) local pos, node, meta, drops = ... + drops[1]:get_meta():from_table({fields = meta}) + if next_pm then return next_pm(...) end + end + return tbl + end; + reg_autopreserve = function(id, tbl) + minetest.register_node(id, lib.node.autopreserve(id, tbl)) + end; + + boxwarp = boxwarp; + boxwarped = boxwarped; +} ADDED mods/vtlib/obj.lua Index: mods/vtlib/obj.lua ================================================================== --- mods/vtlib/obj.lua +++ mods/vtlib/obj.lua @@ -0,0 +1,111 @@ +-- functions for working with entities inexplicably missing +-- from the game API + +local fn = {} +local lib = ... + +-- WARNING: INEFFICIENT AS FUCK +fn.identify = function(objref) --> objectid + for _, o in pairs(minetest.get_connected_players()) do + if objref == o then return o:get_player_name(), 'player' end + end + for id, le in pairs(minetest.luaentities) do + if le.object == objref then return id, 'entity' end + end +end + +fn.handle = lib.class { + __newindex = function(self,key,newval) + local hnd if self.player + then hnd = minetest.get_player_by_name(self._id) + else hnd = minetest.luaentities[self._id] + end + if key == 'id' then + if type(newval) == 'string' then + local p = minetest.get_player_by_name(newval) + if p then + self._id = newval + self.player = true + return + end + end + if minetest.luaentities[newval] then + self._id = newval + self.player = false + else error('attempted to assign invalid ID to entity handle') end + elseif key == 'obj' then + local no, kind = fn.identify(newval) + if no then + self._id = no + if kind == 'player' + then self.player = true + else self.player = false + end + else error('attempted to assign invalid ObjectRef to entity handle') end + elseif key == 'stack' and self.kind == 'item' then + hnd:set_item(newval) + end + end; + __index = function(self,key) + local hnd if self.player then + hnd = minetest.get_player_by_name(self._id) + else + hnd = minetest.luaentities[self._id] + end + if key == 'online' then + return hnd ~= nil + elseif key == 'id' then + if self.player then return nil + else return self._id end + elseif key == 'obj' then + if self.player + then return hnd + else return hnd.object + end + elseif key == 'kind' then + if self.player then return 'player' + elseif hnd.name == '__builtin:item' then return 'item' + else return 'object' end + elseif key == 'name' then + if self.player then return self._id + elseif self.kind == 'item' then + return ItemStack(hnd.itemstring):get_name() + else return hnd.name end + elseif key == 'stack' and self.kind == 'item' then + return ItemStack(hnd.itemstring) + elseif key == 'height' then + if kind == 'item' then return 0.5 + elseif kind == 'player' then + local eh = hnd.object:get_properties().eye_height + return eh and (eh*1.2) or 1 + else + local box = hnd.object:get_properties().collisionbox + if box then + local miny,maxy = box[2], box[5] + return maxy-miny, miny + else return 0 end + end + end + end; + construct = function(h) + local kind, id + if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then + kind = 'player'; + id = h + elseif minetest.luaentities[h] then + kind = 'entity'; + id = h + else id, kind = fn.identify(h) end + + if not id then + error('attempted to construct object handle from invalid value') + end + + return { + player = kind == 'player'; + _id = id; + } + end; +} + +return fn ADDED mods/vtlib/registry.lua Index: mods/vtlib/registry.lua ================================================================== --- mods/vtlib/registry.lua +++ mods/vtlib/registry.lua @@ -0,0 +1,147 @@ +local register = {} +local registry = { + defercheck = function() return true end + -- used to warn about deferments that have not been discharged by a certain threshold +} +registry.mk = function(name,db) + local reg = {} + if not db then -- auxiliary db, stored in registry itself + reg.db = {} + db = reg.db + end + local dat = { + iters = {}; + state = {}; + defer = {}; + } + reg.invoke = function(fnid,tgtid) + local fno = dat.iters[fnid] + if not fno then return false end + + local runid = fnid .. '@' ..tgtid + if dat.state[runid] then return true end + + if fno.deps then for k,f in pairs(fno.deps) do + if reg.invoke(f,tgtid) == false then return false end + end end + + fno.fn(tgtid, db[tgtid]) + + dat.state[runid] = true + return true + end + reg.foreach = function(ident,deps,fn) + dat.iters[ident] = {deps = deps, fn = fn} + for k in pairs(db) do + if reg.invoke(ident,k) == false then + -- not all dependencies are available + -- to run yet; must defer until then + dat.defer[#dat.defer+1] = ident + return false + end + end + for i,dfn in pairs(dat.defer) do + local deferred = dat.iters[dfn] + for _,d in pairs(deferred.deps) do + if not dat.iters[d] then goto skipdfmt end + end + -- a deferred function can now be run, do so + table.remove(dat.defer,i) + reg.foreach(dfn,deferred.deps,deferred.fn) + ::skipdfmt::end + + return true + end + reg.link = function(key,value) + -- support simple arrays as well as kv stores + if value == nil then + value = key + key = #db+1 + end + db[key] = value + for id in pairs(dat.iters) do + reg.invoke(id,key) + end + return value + end + reg.meld = function(tbl) + for k,v in pairs(tbl) do reg.link(k,v) end + end + register[name] = reg + + local nextfn = registry.defercheck + registry.defercheck = function() + if #dat.defer ~= 0 then + print('WARNING: ' .. tostring(#dat.defer) .. ' deferred iterator(s) have not yet been discharged for registry “' .. name .. '”') + local log = print + for i,v in pairs(dat.defer) do + log('\t' .. tostring(i) .. ') ' .. v) + end + log('there is likely a missing dependency or dependency ordering problem. also make sure you have spelled the names of the iterator dependencies correctly') + return false + end + nextfn() + end + return reg +end + +return registry + +--[[ +if sorcery.DEBUG then + local function dump(tbl,indent) + indent = indent or 0 + local space = string.rep(' ',indent*4) + for k,v in pairs(tbl) do + if type(v) == 'table' then + print(string.format('%s%s = {',space,k)) + dump(v,indent + 1) + print(string.format('%s}', space)) + else + print(string.format('%s%s = %q',space,k,tostring(v))) + end + end + end + + local metals = { + oregonium = {desc = 'Oregonium'}; + nevadite = {desc = 'Nevadite'}; + } + local myreg = sorcery.registry.mk('metals',metals) + + sorcery.register.metals.link('californium', { + desc = "Californium"; + }) + + sorcery.register.metals.foreach('sorcery:mkingot',{'sorcery:mkfrag'}, function(k,v) + local ingot = 'sorcery:' .. k .. '_ingot' + print('registered',ingot) + v.parts = v.parts or {} + v.parts.ingot = ingot; + end) + + sorcery.registry.defercheck() + + sorcery.register.metals.link('washingtonium', { + desc = "Washingtonium"; + }) + + sorcery.register.metals.foreach('sorcery:mkfrag',{}, function(k,v) + local fragment = 'sorcery:' .. k .. '_fragment' + print('registered',fragment) + v.parts = v.parts or {} + v.parts.fragment = fragment; + end) + + sorcery.register.metals.link('biloxite', { + desc = "Biloxite"; + }) + + dump(metals) + if sorcery.registry.defercheck() then + print('lingering deferments!') + else + print('all good!') + end +end +]] ADDED mods/vtlib/str.lua Index: mods/vtlib/str.lua ================================================================== --- mods/vtlib/str.lua +++ mods/vtlib/str.lua @@ -0,0 +1,242 @@ +local sanitable = { + from = { + ['\xfe'] = '\xf0'; + ['\1'] = '\xf1'; + ['\2'] = '\xf2'; + ['\3'] = '\xf3'; + ['\0'] = '\xf4'; -- NULs apparently can't be saved in sqlite, + -- or possibly just player metadata + }; + + to = { + ['\xf0'] = '\xfe'; + ['\xf1'] = '\1'; + ['\xf2'] = '\2'; + ['\xf3'] = '\3'; + ['\xf4'] = '\0'; + }; +} + +local utf8 +if _G.minetest then + if minetest.global_exists 'utf8' then + utf8 = _G.utf8 + end +else + utf8 = _G.utf8 +end +if not utf8 then -- sigh + utf8 = {} + local bptns = { + {0x80, 0x00}; + {0xE0, 0xC0}; + {0xF0, 0xE0}; + {0xF8, 0xF0}; + } + local function bl(n) + for i = 1,4 do + local mask, ptn = bptns[i][1], bptns[i][2] + if bit.band(mask,n) == ptn then + return i, bit.bnot(mask) + end + end + -- invalid codepoint + end + local function ub(bytes, ofs) ofs = ofs or 0 + local function B(n) return string.byte(bytes, n+ofs) end + local eb, m1 = bl(B(1)) + if not eb then return -1 end + local val = bit.band(B(1), m1) + for i = 2,eb do + val = bit.bor(bit.lshift(val, 6), bit.band(0x3F, B(i))) + end + return val + end + function utf8.codepoint(str, n) return ub(str, (n and n-1 or nil)) end + local uMatchPtn = "()([^\x80-\xC1\xF5-\xFF][\x80-\xBF]*)" + function utf8.codes(s) + local nx = string.gmatch(s,uMatchPtn) + return function() + local pos,bytes = nx() + if not pos then return nil end + local by = ub(bytes) + return pos, by + end + end + function utf8.len(s) + local i = 0 + for _ in utf8.codes(s) do i=i+1 end + return i + end + function utf8.char(s, ...) + local v + if s <= 0x7F then + v = string.char(s) + elseif s <= 0x7FF then + v = string.char( + bit.bor(0xC0, bit.rshift(s,6)), + bit.bor(0x80, bit.band(s, 0x3F)) + ) + elseif s <= 0xFFFF then + v = string.char( + bit.bor(0xE0, bit.rshift(s,6*2) ), + bit.bor(0x80, bit.band(bit.rshift(s,6), 0x3F)), + bit.bor(0x80, bit.band( s, 0x3F)) + ) + elseif s <= 0x10FFFF then + v = string.char( + bit.bor(0xF0, bit.rshift(s,6*3) ), + bit.bor(0x80, bit.band(bit.rshift(s,6*2), 0x3F)), + bit.bor(0x80, bit.band(bit.rshift(s,6*1), 0x3F)), + bit.bor(0x80, bit.band( s, 0x3F)) + ) + else -- invalid byte + v="" + end + if select('#', ...) > 0 then + return v, utf8.char(...) + else return v end + end +end + +local function nToStr(n, b, tbl) + local str = {} + local i = 0 + if n < 0 then + n = -n + str[1] = tbl['-'] + i = 1 + end + if n == 0 then return tbl[0] else repeat i = i + 1 + local v = n%b + n = math.floor(n / b) + str[i] = assert(tbl[v]) + until n == 0 end + return table.concat(str) +end + +return { + utf8 = utf8; + + capitalize = function(str) + return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2) + end; + + beginswith = function(str,pfx) + if #str < #pfx then return false end + if string.sub(str,1,#pfx) == pfx then + return true, string.sub(str,1 + #pfx) + end + end; + + endswith = function(str,sfx) + if #str < #sfx then return false end + if string.sub(str,#sfx) == sfx then + return true, string.sub(str,1,#sfx) + end + end; + + explode = function(str,delim,pat) -- this is messy as fuck but it works so im keeping it + local i = 1 + local tbl = {} + if pat == nil then pat = false end + repeat + local ss = string.sub(str, i) + local d + if pat then + local matches = {string.match(ss, '()' .. delim .. '()')} + if #matches > 0 then + local start,stop = matches[1], matches[#matches] + d = start + i = i + stop - 1 + end + else + local dl = string.len(delim) + d = string.find(ss, delim, 1, not pat) + if d then i = i + d + dl - 1 end + end + if not d then + tbl[#tbl+1] = string.sub(ss,1,string.len(ss)) + break + else + tbl[#tbl+1] = string.sub(ss,1,d-1) + end + until i > string.len(str) + return tbl + end; + + rand = function(min,max) + if not min then min = 16 end + if not max then max = min end + local str = '' + local r_int = 0x39 - 0x30 + local r_upper = r_int + (0x5a - 0x41) + local r_lower = r_upper + (0x7a - 0x61) + for i = 1,math.random(max - min) + min do + -- 0x30 -- 0x39 + -- 0x41 -- 0x5A + -- 0x61 -- 0x71 + local codepoint = math.random(r_lower) + if codepoint > r_upper then + codepoint = (codepoint - r_upper) + 0x61 + elseif codepoint > r_int then + codepoint = (codepoint - r_int) + 0x41 + else + codepoint = codepoint + 0x30 + end + str = str .. string.char(codepoint) + end + return str + end; + + chop = function(str) + if string.sub(str, 1,1) == ' ' then + str = string.sub(str, 2) + end + if string.sub(str, #str,#str) == ' ' then + str = string.sub(str, 1, #str - 1) + end + return str + end; + + meta_armor = function(str,mark_struct) + -- binary values stored in metadata need to be sanitized so + -- they don't contain values that will disrupt parsing of the + -- KV store, as minetest (stupidly) uses in-band signalling + local sanitized = string.gsub(str, '.', function(char) + if sanitable.from[char] then + return '\xfe' .. sanitable.from[char] + else return char end + end) + if sanitized ~= str and mark_struct then + -- use different type code to mark struct headers for + -- back-compat + return string.gsub(sanitized,'^\xfe\xf0\x99','\xfe\x98') + else return sanitized end + end; + meta_dearmor = function(str,cond) + local dearmor = function(s) + return string.gsub(s, '\xfe([\xf0\xf1\xf2\xf3\xf4])', function(char) + return sanitable.to[char] + end) + end + if cond then + if string.sub(str,1,2) == '\xfe\x98' then + return dearmor(string.gsub(str,'^\xfe\x98','\xfe\xf0\x99')) + else return str end + else return dearmor(str) end + end; + + nExp = function(n) + return nToStr(n, 10, { + [0]='⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'; + ['-'] = '⁻', ['('] = '⁽', [')'] = '⁾'; + }) + end; + nIdx = function(n) + return nToStr(n, 10, { + [0]='₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'; + ['-'] = '₋', ['('] = '₍', [')'] = '₎'; + }) + end; +} ADDED mods/vtlib/tbl.lua Index: mods/vtlib/tbl.lua ================================================================== --- mods/vtlib/tbl.lua +++ mods/vtlib/tbl.lua @@ -0,0 +1,259 @@ +local lib = ... + +local fn = {} + +fn.shuffle = function(list) + for i = #list, 2, -1 do + local j = math.random(i) + list[i], list[j] = list[j], list[i] + end + return list +end + +fn.scramble = function(list) + return fn.shuffle(table.copy(list)) +end + +fn.urnd = function(min,max) + local r = {} + for i=min,max do r[1 + (i - min)] = i end + fn.shuffle(r) + return r +end + +fn.uniq = function(lst) + local hash = {} + local new = {} + for i,v in ipairs(lst) do + if not hash[v] then + hash[v] = true + new[#new+1] = v + end + end + return new +end + +fn.copy = function(t) + local new = {} + for i,v in pairs(t) do new[i] = v end + setmetatable(new,getmetatable(t)) + return new +end + +fn.deepcopy = table.copy or function(t) + new = {} + for k,v in pairs(t) do + if type(v) == 'table' then + new[k] = fn.deepcopy(v) + else + new[k] = v + end + end + return new +end + +fn.append = function(r1, r2) + local new = fn.copy(r1) + for i=1,#r2 do + new[#new + 1] = r2[i] + end + return new +end + +fn.merge = function(base,override) + local new = fn.copy(base) + for k,v in pairs(override) do + new[k] = v + end + return new +end + +fn.deepmerge = function(base,override,func) + local new = {} + local keys = fn.append(fn.keys(base),fn.keys(override)) + for _,k in pairs(keys) do + if type(base[k]) == 'table' and + type(override[k]) == 'table' then + new[k] = fn.deepmerge(base[k], override[k], func) + elseif func and override[k] and base[k] then + new[k] = func(base[k],override[k], k) + elseif override[k] then + new[k] = override[k] + else + new[k] = base[k] + end + end + return new +end + +fn.has = function(tbl,value,eqfn) + for k,v in pairs(tbl) do + if eqfn then + if eqfn(v,value,tbl) then return true, k end + else + if value == v then return true, k end + end + end + return false, nil +end + +fn.keys = function(lst) + local ks = {} + for k,_ in pairs(lst) do + ks[#ks + 1] = k + end + return ks +end + +fn.pick = function(lst) + local keys = fn.keys(lst) + local k = keys[math.random(#keys)] + return k, lst[k] +end + +fn.unpack = table.unpack or unpack or function(tbl,i) + i = i or 1 + if #tbl == i then return tbl[i] end + return tbl[i], fn.unpack(tbl, i+1) +end + +fn.split = function(...) return fn.unpack(lib.str.explode(...)) end + +fn.each = function(tbl,f) + local r = {} + for k,v in pairs(tbl) do + local v, c = f(v,k) + r[#r+1] = v + if c == false then break end + end + return r +end + +fn.each_o = function(tbl,f) + local keys = fn.keys(tbl) + table.sort(keys) + return fn.each(keys, function(k,i) + return f(tbl[k],k,i) + end) +end + +fn.iter = function(tbl,fn) + for i,v in ipairs(tbl) do fn(v, i) end +end + +fn.map = function(tbl,fn) + local new = {} + for k,v in pairs(tbl) do + local nv, nk = fn(v, k) + new[nk or k] = nv + end + return new +end + +fn.fold = function(tbl,fn,acc) + if #tbl == 0 then + fn.each_o(tbl, function(v) + acc = fn(acc, v, k) + end) + else + for i,v in ipairs(tbl) do + acc = fn(acc,v,i) + end + end + return acc +end + +fn.walk = function(tbl,path) + if type(path) == 'table' then + for _,p in pairs(path) do + if tbl == nil or tbl[p] == nil then return nil end + tbl = tbl[p] + end + else + tbl = tbl[path] + end + return tbl +end + +fn.proto = function(tbl,proto) + local meta = getmetatable(tbl) + local nm = {__index = proto or tbl} + if meta ~= nil then + nm = table.copy(meta) + nm.__index = proto + nm.__metatable = meta + end + return setmetatable(tbl or {},nm) +end +fn.defaults = function(dft, tbl) + tbl = tbl or {} + local rp = {} + for k,v in pairs(dft) do + if tbl[k] == nil then rp[k] = v end + end + return fn.proto(rp, tbl) +end + +fn.case = function(e, c) + if type(c[e]) == 'function' + then return (c[e])(e) + else return c[e] + end +end + +fn.cond = function(exp, c) + for i, v in ipairs(c) do + if c[1](exp) then return c[2](exp) end + end +end + +fn.strmatch = function(tbl, str) + if tbl == str then return true end + if type(tbl) == 'string' then return false end + return fn.has(tbl, str) +end + +fn.select = function(tbl, prop, ...) + local keycache + local check if type(prop) == 'function' then + check = prop + keycache = ... + else + local val val, keycache = ... + check = function(ent) return ent[prop] == val end + end + for k,v in pairs(tbl) do + if (not keycache) or (not keycache[k]) then -- help avoid expensive selectors + if check(v,k) then + if keycache then keycache[k] = true end + return v, k + end + end + end +end + +fn.setOrD = function(set, a, ...) + set[a] = true + if select('#', ...) == 0 then return set end + fn.setOrD(set, ...) +end + +fn.setAndD = function(set, ...) + local t = {} + local function iter(a, ...) + if set[a] then t = true end + if select('#', ...) == 0 then return end + iter(...) + end + iter(...) + return t +end + +fn.set = function(...) + local s = {} + fn.setOrD(s, ...) + return s +end + + +return fn ADDED mods/vtlib/tree.lua Index: mods/vtlib/tree.lua ================================================================== --- mods/vtlib/tree.lua +++ mods/vtlib/tree.lua @@ -0,0 +1,75 @@ +local lib = ... +local tree = {} + +local let let = lib.class { + __index = { + get = function(self, v) + if self.bind[v] ~= nil then + return self.bind[v] + elseif self.import ~= nil then + for i=#self.import, 1, -1 do + local g = self.import[i]:get(v) + if g then return g end + end + end + if self.parent ~= nil then + return self.parent:get(v) + else return nil end + end; + put = function(self, v, val) + self.bind[v] = val + end; + unify = function(self, v, val) + local x = self:get(v) + if x == nil then self:put(v, val) + return true + elseif x == val then return true + else return false end + end; + newBinds = function(self) + return pairs(self.bind) + end; + binds = function(self) + local k + return function() + k = next(self.bind, k) + if k == nil then + self = self.parent + if not self then return nil end + k = next(self.bind, k) + end + return k, self.bind[k] + end + end; + compile = function(self, into) + -- assemble the entire tree into a single unified list, + -- suitable for returning from a completed query. uses + -- tail recursion for optimal performance + into = into or {} + for k,v in pairs(self.bind) do + into[k]=into[k] or v + end + if self.parent then -- branch node + return self.parent:compile(into) + else -- root node in let tree + return into + end + end; + branch = function(self, ref) return let(self,ref) end; + import = function(i) + if self.import == nil then self.import = {i} + else table.insert(self.import, i) end + end; + }; + construct = function(parent, ref) + return { + bind = {}; + parent = parent; + import = nil; + ref = ref; -- used to store e.g. a position in a program + } + end; +} +tree.let = let + +return tree ADDED mods/vtlib/ui.lua Index: mods/vtlib/ui.lua ================================================================== --- mods/vtlib/ui.lua +++ mods/vtlib/ui.lua @@ -0,0 +1,82 @@ +local l = ... + +return { + form = l.class { + name = 'form'; + __index = { + nl = function(self, h) + h = h or 0 + self.curs.x = 0 + self.curs.y = math.max(h, self.curs.y + self.curs.maxh) + self.curs.maxh = 0 + end; + attach = function(self, elt, x, y, w, h, ...) + local content = '' + if self.width - self.curs.x < w then self:nl() end + for _, v in pairs{...} do + content = content .. ';' .. minetest.formspec_escape(tostring(v)) + end + self.src = self.src .. string.format('%s[%f,%f;%f,%f%s]', elt, x,y, w,h, content) + if h > self.curs.maxh then self.curs.maxh = h end + end; + add = function(self, elt, w, h, ...) + local ax, ay = self.curs.x, self.curs.y + self:attach(elt, ax,ay, w,h, ...) + self.curs.x = self.curs.x + w + self.pad + if self.curs.x > self.width then self:nl() end + return ax, ay + end; + render = function(self) + return string.format("size[%f,%f]%s", self.width, self.curs.y + self.pad + self.curs.maxh, self.src) + end; + }; + __tostring = function(self) return self:render() end; + construct = function() + return { + src = ""; + width = 8; + pad = 0; + curs = {x = 0, y = 0, maxh = 0}; + } + end; + }; + + tooltipper = function(dui) + -- takes a configuration table mapping affinities to colors. + -- 'neutral' is the only required affinity + return function(a) + local color = a.color and a.color:readable(0.65, 1.0) + if color == nil then color = l.color(136,158,177) end + local str = a.title + if a.desc then + str = str .. '\n' .. color:fmt(minetest.wrap_text(a.desc,60)) + end + if a.props then + -- str = str .. '\n' + for _,prop in pairs(a.props) do + local c + if prop.color and l.color.id(prop.color) then + c = prop.color:readable(0.6, 1.0) + elseif dui.colors[prop.affinity] then + c = l.color(dui.colors[prop.affinity]) + else + c = l.color(dui.colors.neutral) + end + + str = str .. '\n ' .. c:fmt('* ') + + if prop.title then + str = str .. c:brighten(1.2):fmt(prop.title) .. ': ' + end + + local lines = minetest.wrap_text(prop.desc, 55, true) + str = str .. c:fmt(lines[1]) + for i=2,#lines do + str = str .. '\n' .. string.rep(' ',5) .. c:fmt(lines[i]) + end + end + end + return color:darken(0.8):bg(str) + end; + end; +} ADDED mods/vtlib/vtlib.ct Index: mods/vtlib/vtlib.ct ================================================================== --- mods/vtlib/vtlib.ct +++ mods/vtlib/vtlib.ct @@ -0,0 +1,8 @@ +# vtlib +this is a standalone library of critical utility functions i, velartrill, would never want to write a game or mod without. vtlib can be renamed; it will register itself in a global matching its mod name. it is assumed you will be using it under the name of vtlib for the purposes of documentation + +## primitive munging + +### vtlib.math +### vtlib.str +### vtlib.tbl ADDED settingtypes.txt Index: settingtypes.txt ================================================================== --- settingtypes.txt +++ settingtypes.txt @@ -0,0 +1,22 @@ +[starsoul] + +[*genesis] +# scenario used for new games. this is a horrible hack used until i +# can figure out some way to get a proper start-game menu in +starsoulPlayerScenario (player scenario) enum imperialExpat imperialExpat,gentlemanAdventurer,terroristTagalong,tradebirdBodyguard + +[*multiplayer] +# use the fatigue mechanic as an antipoopsocking measure, requiring +# players to go to bed (and presumably log out) in order to recover +# wakefulness. when disabled, an alternate fatigue mechanic is used +# where players slowly recover wakefulness during prolonged periods +# of rest (zero stamina consumption). +starsoulMultiplayersMustSleep (require sleep) bool true + +# when sleep is enabled, require players to actually right-click on +# their bed before logging out in order to obtain sleep credit. +# otherwise, fatigue will decrease whenever players are simply +# logged out. +# +# Requires: starsoulMultiplayersMustSleep +starsoulMultiplayersNeedBed (sleep requires bed) bool true ADDED src/lore.ct Index: src/lore.ct ================================================================== --- src/lore.ct +++ src/lore.ct @@ -0,0 +1,132 @@ +# starsoul lore +! spoilers ahoy! + +## Thinking Few +the Galaxy teems with life, but only one in a trillion of its creatures is fully sophont, with a soul and mentality of their own. + +### Lesser Races +the majority of the Thinking Few are held in thrall to the Starsouled, their null psionic potential locking them out of the higher levels of civilizational power and attainment. + +#### Humans +The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greater Races by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation all but unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche. + +#### Kruthandi +The Kruthandi are a race of four-armed marsupialoids, with the rough body proportions of meerkats, though much larger. + +Geographically, they occupy a small net of systems linked only through their home system. Unable to accept the reality that their lack of psionics doomed them to a subservient, sublight role, the Kruthandi indulged in a brief, petulant, and entirely futile war with a Su'ikuri state[^lizard-war], and then retreated to their home web to sulk. Access to their space is tightly controlled, and psionic races are absolutely barred from their worlds, even the mere Greater Races, tho this is hilariously unenforcible. In practice they are particularly vulnerable to psicrime – when you ban psionics, only criminals will have mind powers. The Kruthandi are generally viewed with pity and amusement as a pathetic basket case of a civilization, and engage in little intercourse with the broader galaxy. + + lizard-war: The Su'ikuri sovereign in question was of a decidedly philosophical bent and was commendably gentle dealing with the upstarts, seeming more bemused than angered by the attack. He took a few of their leaders as battle trophies and spanked the remainder out of his system. There were no casualties on either side. + +However, the Kruthandi are an ancient and sophisticated civilization, and there is much more to their culture than mere wallowing in victimhood (though this certainly has a special place in the Kruthandi heart). Their hate-crush on the Starsouled Races has lead to an utter obsession with metric technology, and the Kruthandi have, by sheer brute force and fanatic tenacity, built a surprisingly sophisticated gravitics industry. While this makes little economic sense, it soothes the Kruthandic psyche. + +#### Qir +a race of religious fanatics cleaving to no particular faith, Qir enthusiastically adopt and syncretize new religions as fast as Heaven can churn out prophets. their fanaticism seems to be a cultural evolution to compensate for exceptionally weak mememmune systems; unable to properly critique new ideas, the Qir need to outsource much of their reasoning to systems of sacred commandments, ideally those developed by species who know how to deal with memes. good old-fashioned natural selection does the rest. + +### Greater Races +a handful of species have souls that are just barely capable of developing external psionic channels. the respected vassals of the Starsouled, they can touch the spirits of others, sending messages and reading thoughtforms -- or attacking with torturous sensations and ruinous emotion. however, their power does not extend to the physical universe; they rely entirely on their Starsouled masters for access to faster-than-light travel. + +### Starsouled +the unquestioned lords of space and time, the Starsouled Races are those lucky few to have evolved cognitive architectures that allow a soul to reach its full development potential, progressing from merely cogitating about the universe to manipulating it directly with innate power. seemingly as a consequence of the necessary neural architecture, the Starsouled are all ferociously intelligent -- IQs below 150 are unheard of. however, they are marred by a proportional tendency toward mental instability and psychosis. + +their civilizations are known as the Powers. + +#### Su'ikuri +(sg. Su'ikutra, adj. Su'ikuruk) +a reptilian race of artists, aesthetes, hedonists, monks, and philosophers, the Su'ikuri are an idle, contemplative, and aristocratic people whose massive psionic sophistication numbers them among the Powers -- much to the annoyance of the Eluthrai. as any adult has the requisite level of finesse and raw power to tweak individual alleles throughout the whole body of a living organism, the Su'ikuri are a race of peerless organgineers. they eschew "dead" hylotechnology, and insist on using biotech wherever remotely practicable. + +their 'spacecraft' are massive tree-like organisms housing whole ecosystems, propelled and protected against radiation by the psionic power of their crew. sometimes they are equipped with technology produced by a vassal race, but only when unavoidable. + +Su'ikuri generally use Lesser Races for manual labor, and Greater Races to overseer these laborers. whether these are paid and respected laborers or outright slaves depends entirely on the ethos of the local civilization. + +Su'ikuruk society is strictly feudal, with a hierarchy based on psionic skill and wit. virtually all conflict is resolved with either a polite, prolonged philosophical debate (the Su'ikuruk version of a duel) or a brute psionic struggle -- the party overpowered by the greater psion is compelled to submit totally, and may achieve freedom only by strengthening their soul to the point of being able to overpower their former superior. even other members of other Starsouled Races can wind up enslaved this way. + +Su'ikuri relations with the Eluthrai are, as a rule, extremely strained, and many small but high-energy wars have been fought between the Su'ikuruk Powers and the Corcordance. + +a motivated and talented Su'ikutra can reach astropathic levels of psionic power with only a century of practice, something otherwise unheard of among Starsouls. + +#### Usukwinya +(sg. Usukwinti, adj. Usuk) + +the Usukwinya, known affectionately as the "Tradebirds", are a psionic avian race. their adults range in height from 1 to 1.3m, and 20-30kg in weight. they lack precise manipulatory appendages and are physically weak, forcing them to rely heavily on their psionics for everyday dasks. however, they remain fully capable of flight even without psionic assistance. + +culturally, the Usukwinya are a mercantile race. they exert their power not through physical force, but through obscene wealth, garnered by selling their painstakingly [^drm value-engineered] technologies to the highest bidder. some rare few Usukwinya will also rent out their psionics, even to the Lesser Races (if they can afford their prodigious fees). + drm: Usukwinya DRM is some of the most powerful in the Reach. + +the enthusiastic capitalism of the Usukwinya is tempered by a hardwired loyalty drive so powerful that before First Contact they had no concept of contract law. they are also noteworthy for having never fought a war among themselves, and seem utterly unwilling to resort to force unless physically provoked. their governments all work diligently to maintain peace among the other races, and the somewhat absurd spectacle of a Starsouled diplomat gently negotiating with two hysterical Lesser ambassadors tends to crop up when two factions the Usukwinya have good relationships with threaten war on one another. (the Eluthrai find this patently ridiculous, and prefer to maintain peace with a judicious application of preventive violence. several Usukwinya-organized peace conferences have dissolved when the Eluthrai summarily shattered the offending governments without a note of forewarning.) + +their willingness to trade with or work for anyone and everyone mean that the Usukwinya are the main reason the Lesser Races have any ability to travel beyond the Great Web. however, Usuk astropaths are very selective: they will not use their powers to help their employers to commit acts of aggression, no matter how much you offer to pay them. many human captains chafe under the restrictions of their Tradebird astropaths, but short of relativistic travel, they have no other way to escape the confines of the Web. + +Usukwinya get along with everyone and make excellent diplomats, so long as they can restrain their urge to make a quick profit at the first available opportunity. + +#### Eluthrai +(sg. Eluthra) + +the greatest and most aloof of the living Starsouled races, the Eluthrai are a race of psionic warrior-poets. they are slim humanoids with subtly iridescent dark grey skin, lustrous white hair, red~violet eyes, and tapered, expressive ears. they are very few in number, with no more than ten thousand Eluthrai in the entire galaxy. it is popularly said, not without some reason, that the only reason the Eluthrai haven't conquered that entire galaxy is because they don't care to. + +natural immortals with a very low reproduction rate, the Eluthrai all have an exceptionally long-term worldview that frequently confounds mortal morals. little about them is known for certain, and they interact with the non-Starsouled very rarely, usually to deliver some form of unforeseen intervention that they typically refuse to explain. + +the Eluthrai see themselves as the masters of the Thinking Few, and spare no expense in ensuring they maintain their position. to them, the Great Web is a garden, a place to be tended carefully and protected from the storms outside. their civilization is dedicated to combatting extra-Web threats -- in particular, guarding against the possible return of the Forevanishers. they have cultivated a strong & highly spiritual warrior ethos in consequence + +within the Web itself, they mostly by clandestine means, using "Agents" selected from the Greater (and, occasionally, Lesser) Races to act on their behalf. in general they act directly only when overwhelming force is required, such as to exclude the Kuradoqshe, or to excise Suldibrand. + +it is known that the Eluthrai are of great intelligence: a 200pt IQ makes you a laughable simpleton in their eyes. it is estimated that the average individual has an IQ of 290, close to the theoretical maximum where organized intelligence dissolves into a sea of blinding psychosis. consequently, they are very conservative and cautious of new ideas; their culture emphasises skepticism and avoiding rash action. + +early Eluthran history was extremely warlike, and they could have easily devastated the whole of the Reach in their fanatical pursuit of competing ideologies. however, a philosophical tradition emerged from the rubble of a particularly ruinous exchange that offered the correct tools for neutering the more dangerous aspects of their intelligence -- after the centuries proved its value, the Philosophers exterminated all the remaining Eluthrai who had not adopted their practices. it was a coldblooded but rational act of genocide: an individual Eluthra is intelligent enough to bootstrap an industrial civilization from first principles with a few years of effort. an entire civilization of them, devoid of self-control? that wasn't merely a threat to the Philosophers; it was a threat to the Galaxy entire. + +the Eluthrai have a single common language, Eluthric, which they use in interstellar discourse and in the sciences. however, the different far-flung colonies have their own individual tongues as well. Eluthric has the largest vocabulary of any known language, with over twenty million words. an Eluthra who hasn't learned at least a million of them by adolescence is deemed slow. + +they have developed very slowly since the Philosophers came to power, but were already so advanced that nobody is expected to exceed them any time soon. + +Eluthran civilization is united under the rule of the Philosopher-King, an enlightened despot with unrestricted power, in a complex web of fealty and patronage collectively named the Corcordance of the Eluthrai. while the First Philosopher died tens of thousands of years ago, he had the foresight to prepare a successor to take his place in case of his assassination or ill-fortune. in all those years, power has changed hands only three times. the current Philosopher-King has ruled for eight thousand years. + +Eluthrai have two genders, and dramatic dimorphism. their women are much more intelligent than their men, and proportionately more prone to psychosis. traditionally most of their societies were matriarchal -- with the brains and psionic brawn to overpower the males, there was very little that could keep the Clan-Queens from exerting their will. the First Philosopher recognized however that the lesser intelligence of men was useful, due to their stabler psyches, and proposed patriarchy as part of his solution. this was made possible through a previously obscure psionic technique known as quelling -- with enough intimate exposure to the soul of another, it becomes possible to negate their psionics, even if that psion is stronger. + +among the modern Concordant Eluthrai, a female's mate is expected to be capable of quelling her psionics. female Eluthrai generally cooperate with the practice; it is difficult to learn to quell someone who actively tries to stymie you. it is widely understood, however, that the female sex will only cooperate so long as their men rule wisely: in a celebrated case on a far-flung world where the men began to take too many liberties, the women carefully organized to overpower one another's mates and instituted a compensatory subjugation of the local males for a proportionate period, which the Philosopher-King himself agreed was the just and proper punishment. + +Eluthran technology can be tidily summarized as "uncompromising." the Eluthrai demand excellence from their machines as much as one another, and will happily incur absurd expense to eliminate the smallest flaw. languishing for thousands of years of under such attentions, abetted by the most ferocious living intelligence to be found in the Reach, has created a technological ecosystem that is succeeded in its phenomenal capabilities only by its preposterous expense. an Eluthran computer requires about ten times the time and a hundred times the energy input to fabricate as does a conventional human computer, despite the vast gulf in manufacturing capabilities, but you can be [!damn] sure it'll still be working in ten million years' time. + +they enjoy a post-scarcity economy that is the envy of even the other Starsouled. + +very few of the Greater Races, and vanishingly few of the Lesser Races, have ever had the opportunity to visit an Eluthran world. they admit only their mysterious Agents and the occasional individual subjected to penal servitude for some great crime against the interests of "the Garden". + +while even their females are not nearly the psionic match of the Su'ikuri, they are nonetheless vastly powerful. their psionics are not as seamlessly integrated into their nervous system as in Usuk neurology, and deploying their power is consequently more effortful, requiring some concentration and intent, but they can bring far more energy to bear. where the Usukwinya have finesse and the Su'ikuri have brute power, the Eluthrai have technique: they can do things with their psionics that the other races never would have imagined possible. + +the Eluthrai put a great deal of effort into foremodeling the universe, seeking to predict future events and trends. their models are far from infallible, but reliable enough that some supersitious Lessers have come to believe that Eluthran psionics can be used to see the future. intelligence-gathering is in the modern era the prime industry of that exalted race, second only to warfare. + +### Forevanished Ones + +#### Forevanishers +a mysterious race or power thought to be responsible for exterminating a number of Forevanished Ones. no one knows for sure that they have themselves Vanished -- for all we know, they could be one of the contemporary Powers… + +#### firstborn +the architects of the Great Web, the Firstborn were the first civilization of which traces remain within the Reach. their psionic and scientific mastery, developed over ten million years of energetic industry, reached levels even the greatest of the modern Starsouled Races cannot hope to equal. while their Continuum Bridges form the backbone of the Lesser civilizations, little else of their manufacture seems to have survived into the present era. + +practically every trace of their existence that does remain is scored with weaponsfire. + +the artifact which tore an external channel into the player's soul in the backstory is of Firstborn design and uncertain purpose. Commune scholars had hitherto ascertained only that it was a machine seemingly able to produce psionic effects -- something that [!should] have been a contradiction in terms. + +! possible plot: the Firstborn devised a means to produce psionic effects with carefully cultured neurons embedded in a mechanical matrix. essentially creating slaved psionic AI dedicated to a single purpose. while these rudimentary consciousnesses, barely fit to called souls, did not suffer, some other race or perhaps a faction among the Firstborn seems to have taken exception to the practice of trapping souls in metal, outside the thread of reincarnation, and exterminated the civilization to prevent its heresy. +! if this was a real proper AAA game the player would face some epic choice to release the secret and free the Lesser Races (or a subset of them) from the dominion of the Starsouled, turning psionics into a mere commodity; keeping the secret but placing her power at the disposal of the Commune (or Empire, in return for elevation to the ranks of nobility); or joining the Eluthrai as an honorary citizen in recompense for keeping the secret. alas, i don't have a budget. + +## psionics +the ability of the soul to extend its will beyond the confines of its substrate. this power is technically defined as the presence of one or more external psionic channels in the structure of the soul. such a channel allows the soul to direct excess numina into other souls or into the numon field of the physical universe. + +the delicate interlink between soul and body relies on quantum phenomena, and only carbon-based life seems able to maintain such a link. silicon-based intelligence is at most a simulacrum of true thought. + +### farspeakers +the most powerful psions among the Greater Races, the Farspeakers can extend their mentality across light-years of space, providing FTL comms. such long-range comms require a bonded psion on the receiving end, and cannot feasible reach much beyond a parsec; consequently, long-range FTL communication requires complex networks of bonding. + +### astropaths +the greatest -- and rarest -- of all Starsouls, astropaths can, with technological assistance, manipulate and direct so much numina that they can perform metric conjunction across light-years of distance. a single astropath usually requires a week or more to recover from a fold and regain enough power to perform another, though the greatest Su'ikuri astropaths require only a few days. + +## culture +the Galaxy is an intensely cosmopolitan place. while a few of the Lesser Races keep to themselves, most intermix to varying degrees. this is largely necessary due to the intense specialization of most races. there are some tasks a Qir cannot undertake without the help (however grudging) of a Kruthandi, and vice-versa. before humans burst onto the scene, rescued by the Qir from a state of savagery on a long-lost Webworld, most scientific advances and engineering breakthrough were a result of either Eluthran super-science or scholars of multiple races working together. + +### faith +faith is at once fantastically diverse and largely uniform. certain ancient customs are universally acknowledged: even the barbarian humans remembered the Wild Gods (and had fallen so low as to begin worshipping them, in hope of appeasing them and forestalling their wrath). there are two broad "schools" of faith: one, by far the most common, is syncretic and idiosyncratic, with the believer mixing and matching customs, notions, and gods from wherever they might travel, sincerely if awkwardly celebrating alien festivities as their own, and generally Doing In Rome. the other is the religious school: the undertaking of a singular exclusive religio-philosophical system, and reserving one's devotion exclusively to that. the Religious, as opposed to the Faithful, are often scorned for denying the Gods of the Faith, though in truth most are henotheistic in their approach. + +an important distinction is that between the Gods of the Faith and the Wild Gods. the former are the subjects of both organized and disorganized religion, worshipped and plied with incense and sacrifices. the Gods of the Faith are the civilized divinities, who first bestowed thought and reason upon the Thinking Few, and it was only by their grace that we rose from the dream of subsophonce. they are tutelary powers, often of a particular place or science, and they are deeply involved in the affairs and fates of Men. they are the Gods of the Harvest, of War and of Peace, of Computation and Cybernetics. they are beloved, oft invoked, and much celebrated. + +the Wild Gods, by contrast, are the animin powers, the temperamental personifications of nature in its varied aspects. these are the Gods of the Storm, of the Sun, of Lust and Beauty, of Life Itself, of Gravity & the Weak Electric Force. they are the far greater of the divinities, powers incomprehensible even the Gods of the Faith, and their whim is utterly unpredictable. there is no point to the organized worship of them; indeed, one turns only to the Wild Gods in the lowest of desperation, undertaking great acts of sacrifice in the hope of attracting their attention and mercy for the briefest of moments in which to regroup. rather, they are objects of awe, inspiration, and terror. one builds shrines to the Gods of the Faith in hopes of attracting their blessing; one builds shrines to the Wild Gods to ward away their temper. they are invoked only in moments of rage, terror, passion, or sublime awe. to seek connection with them, as some rare few do -- mostly artists and weary, tortured souls -- is a spiritual endeavor, not a religious one, the undertaking of oblivion and dissolution, to lose oneself in the depths of power and passion, and dance among the storm. there is nothing in the universe so powerful, so dangerous, majestic, or beautiful as the Wild Gods: they are truly beyond all knowing. + +it is of note that atheism is a singularly scorned and rare tendency. even the Eluthrai honor the Gods, even if the Lesser Races might be inclined to account them on the same level. ADDED src/sem.ct Index: src/sem.ct ================================================================== --- src/sem.ct +++ src/sem.ct @@ -0,0 +1,23 @@ +# starsoul semantics + +## tool levels +some items have a particular level requirement to enable digging. in general, level 0 should be used for things that can be dug by hand +nanotech can dismantle anything up to level 2 + +* sediment +** 0: sand, dirt (diggable with hand) +* stone +** 0: loose rock (hands) +** 1: brittle minerals (pick) +** 2: hard minerals (jackhammer) +** 3: diamond +* metal +** 1: solid metal +** 2: solid titanium +** 3: solid osmiridium +** 4: solid diamond +** 5: unobtanium +* plant +** 0: leaves, twigs +** 1: branches + ADDED src/sfx/base.orc Index: src/sfx/base.orc ================================================================== --- src/sfx/base.orc +++ src/sfx/base.orc @@ -0,0 +1,21 @@ +; [ʞ] base.orc +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? useful opcodes and defs + +#ifndef BASE_INC +#define BASE_INC #1# + +opcode fade, a, iiii + isdur, isvol, ifin, ifout xin + afade bpf linseg:a(0,isdur,1), 0,0, ifin,isvol, ifout,isvol, 1,0 + xout afade +endop + +opcode pulse, a, ii + idur,iwhen xin + ap bpf linseg:a(0,idur,1), 0,0, iwhen,1, 1,0 + xout ap +endop + +#end ADDED src/sfx/conf.lua Index: src/sfx/conf.lua ================================================================== --- src/sfx/conf.lua +++ src/sfx/conf.lua @@ -0,0 +1,67 @@ +local function find(args) + local cmd = string.format('find . -maxdepth 1 %s -printf "%%f\\0"', args) + local ls = io.popen(cmd, 'r') + local v = ls:read 'a' + ls:close() + local names = {} + for n in v:gmatch '([^\0]+)' do + table.insert(names, n) + end + return names +end + +local polyfx = find '-name "*.n.csd"' +local fx = find '-name "*.csd" -not -name "*.n.csd"' + + +local function envarg(n, default) + local v = os.getenv(n) + if v then return tonumber(v) end + return default +end + +local baseSeed = envarg('starsoul_sfx_vary', 420691917) +local variants = envarg('starsoul_sfx_variants', 4) +local fmt = os.getenv 'starsoul_sfx_fmt' or 'wav' + +local rules = {} +local all = {} +if fmt ~= 'ogg' then table.insert(rules, + 'out/starsoul-%.ogg: %.' .. fmt .. '\n' .. + '\tffmpeg -y -i "$<" "$@"\n') +end +local function rule(out, file, seed) + if fmt == 'ogg' then + table.insert(rules, string.format( + 'out/starsoul-%s.ogg: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' .. + '\tcsound --omacro:seed=%d --format=ogg -o "$@" "$<"\n', + out, file, + seed + )) + else + table.insert(rules, string.format( + '%s.%s: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' .. + '\tcsound --omacro:seed=%d --format=%s -o "$@" "$<"\n', + out, fmt, file, + seed, fmt + )) + end + table.insert(all, string.format('out/starsoul-%s.ogg', out)) +end +for _, v in ipairs(polyfx) do + local bn = v:match '^(.+).n.csd$' + for i=1, variants do + rule(bn .. '.' .. tostring(i), bn .. '.n', baseSeed + 4096*i) + end +end +for _, v in ipairs(fx) do + local bn = v:match '^(.+).csd$' + rule(bn, bn, baseSeed) +end + +local makefile = io.open('makefile', 'w') +makefile:write('all: ', table.concat(all, ' '), '\n') +makefile:write('clean:\n\trm ', '"'..table.concat(all, '" "') .. '"', '\n\n') + +makefile:write(table.concat(rules, '\n')) +makefile:close() ADDED src/sfx/configure.csd Index: src/sfx/configure.csd ================================================================== --- src/sfx/configure.csd +++ src/sfx/configure.csd @@ -0,0 +1,19 @@ +; [ʞ] configure.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for reconfiguring your suit, +; e.g. turning on power save + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"chirp" 0.00 0.10 0.20 20 1600 +i"chirp" + 0.10 0.20 20 1300 + + + ADDED src/sfx/digital.orc Index: src/sfx/digital.orc ================================================================== --- src/sfx/digital.orc +++ src/sfx/digital.orc @@ -0,0 +1,149 @@ +; [ʞ] digital.orc +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? chirpy beepy blarpy noises for UI and computer +; sound effects. + +; conventions +; - all waveforms start with aw* +; useful idioms: +; - current time factor: linseg(0,p3,1) +; std parameters +; p2 = start time +; p3 = duration +; p4 = amp + +#include "base.orc" + +instr rumble + aton jspline 1, 100,500 + ;amp adsr 0.2,0.4,.9, 0.2 + kamp bpf linseg(0,p3,1), \ + 0.0, 0.0, \ + 0.1, 0.8, \ + 0.5, 1.0, \ + 0.7, 0.8, \ + 1.0, 0.0 + + aw poscil p4*kamp, 200 + 100*aton + out aw +endin + +instr spindown + avol fade p3,p4,0.5,0.9 + kpr linseg 0, p3, 1.0 + ; remap time so our spindown starts slowly but rapidly + ; speeds up as the effect progresses + at bpf a(kpr), 0,0, 0.6,0.2, 1,1 + + ; noise component + al = .3 + a(unirand:k(.7)) + af bpf at, 0, 700, .5, 400, 1,200 + aw poscil al*avol, af + + out aw +endin + +instr blarp + apr linseg 0,p3,1 + avol bpf apr, 0,0, 0.3,1, 1,0 + + aff poscil 1.0, 10 + afr bpf aff, \ + 0.0, 400, \ + 0.2, 600, \ + 0.5, 500, \ + 0.6, 700, \ + 0.9, 300, \ + 1.0, 400 + aw poscil avol*p4, afr + out aw +endin + +instr chirp + abeep poscil 1, p5 + abeep bpf abeep, \ ; apply chirp envelope + 0.0, 0.0, \ + 0.4, 0.1, \ + 0.6, 0.9, \ + 1.0, 1.0 + aw poscil abeep*p4, p6 + out aw +endin + +opcode warbulator, kkk, kkk + kfreq, kfi, kfo xin + + kt1 init 0 + kt2 init 0 + kvol poscil 1, kfreq + kvol bpf kvol, kfi,0, kfo,1 + if kvol == 0 then + kt1 = unirand:k(1.0) + kt2 = unirand:k(1.0) + endif + + xout kvol, kt1, kt2 +endop + +instr warble + afade fade p3,p4, 0.1,0.9 + + kvol, kton, k_ warbulator p5, p6,p7 + kton bpf kton, 0,p8, 1,p9 + + aw poscil afade * a(kvol), kton + out aw +endin + +instr warple + afade fade p3,p4, 0.2,0.8 + ap pulse p3,.5 + kv, kwb, kwv warbulator p5, p6, p7 + + atn bpf ap, 0,p8, 1,p9 + ktno = kwb * p10 + av = a(kv) * bpf:a(a(kwv), 0,.2, 0.9,0.3, 1,1) + aw poscil afade*av, atn+a(ktno) + out aw + +endin + +instr wslope + avol fade p3,p4,0.3,0.8 + + afb bpf linseg:a(0,p3,1), 0,p7, 0.5,p8, 1,p9 + afw poscil 1.0, p5 + afw bpf afw, 0,0, 0.3,0.1, 0.7,0.8, 1,1 + afreq = afb - (afw*p6) + + aw poscil avol, afreq + out aw +endin + +instr winddown + avol fade p3,p4,0.1,0.5 + + afmf bpf linseg:a(0,p3,1), 0,100, 0.2,30, 1,1 + afm poscil 1.0, afmf + + afn bpf linseg:a(0,p3,1), 0,1200, 0.5,1000, 0,700 + + aw poscil avol, afn - afm*400 + out aw +endin + +instr blare + avol fade p3,p4,0.03,0.9 + an poscil 1.0, 30 + an bpf an, 0,0, 0.9,0, 1,1 + ar bpf linseg:a(0,p3,1), 0,100, .5,300, 1,400 + aw poscil avol, 500 + (an*ar) + out aw +endin + +instr tune + anois unirand 1 + aw poscil p4, anois * 1000 + out aw +endin ADDED src/sfx/dqual.inc Index: src/sfx/dqual.inc ================================================================== --- src/sfx/dqual.inc +++ src/sfx/dqual.inc @@ -0,0 +1,11 @@ +; [ʞ] dqual.inc [vim: ft=csound] +; ~ lexi hale +; 🄯 public domain / CC0 +; ? default quality settings for Starsoul sfx when +; not overridden in the makefile variables + +sr = 44100 +ksmps = 64 +nchnls = 1 +0dbfs = 1 +seed $seed ADDED src/sfx/error.csd Index: src/sfx/error.csd ================================================================== --- src/sfx/error.csd +++ src/sfx/error.csd @@ -0,0 +1,19 @@ +; [ʞ] error.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? angry blarp fer when ya done a bad + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"blare" 0.00 0.10 0.4 +i"blare" 0.15 0.30 0.6 +e 0.5 + + + ADDED src/sfx/insert-snap.csd Index: src/sfx/insert-snap.csd ================================================================== --- src/sfx/insert-snap.csd +++ src/sfx/insert-snap.csd @@ -0,0 +1,17 @@ +; [ʞ] insert-snap.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? snappy noise for slappin a thing into a slot + + + + +#include "dqual.inc" +#include "physical.orc" + + + +i"snap" 0.00 0.5 0.20 + + + ADDED src/sfx/make.sh Index: src/sfx/make.sh ================================================================== --- src/sfx/make.sh +++ src/sfx/make.sh @@ -0,0 +1,7 @@ +csfmt=wav +baseSeed=420691917 +iters=${iters:-7} + +for mc in *.n.csd; do + +done ADDED src/sfx/mode-nano.csd Index: src/sfx/mode-nano.csd ================================================================== --- src/sfx/mode-nano.csd +++ src/sfx/mode-nano.csd @@ -0,0 +1,22 @@ +; [ʞ] mode-nano.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for activating your nanides + + + + +#include "dqual.inc" +#include "digital.orc" + + + +; common mode switch header +i"chirp" 0.00 0.10 0.10 20 1400 +i"chirp" + 0.20 0.05 10 1100 + +; mode-specific score +i"wslope" 0.20 0.7 0.3 15 200 600 750 900 + + + ADDED src/sfx/mode-off.csd Index: src/sfx/mode-off.csd ================================================================== --- src/sfx/mode-off.csd +++ src/sfx/mode-off.csd @@ -0,0 +1,19 @@ +; [ʞ] mode-nano.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for activating your nanides + + + + +#include "dqual.inc" +#include "digital.orc" + + + +; inverse common mode switch header +i"chirp" 0.00 0.10 0.10 20 1100 +i"chirp" + 0.20 0.05 10 1400 + + + ADDED src/sfx/mode-psi.n.csd Index: src/sfx/mode-psi.n.csd ================================================================== --- src/sfx/mode-psi.n.csd +++ src/sfx/mode-psi.n.csd @@ -0,0 +1,24 @@ +; [ʞ] mode-psi.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for activating your +; t e l e p a t h i c m i n d p o w e r s + + + + +#include "dqual.inc" +#include "digital.orc" +#include "psi.orc" + + + +; common mode switch header +i"chirp" 0.00 0.10 0.10 20 1400 +i"chirp" + 0.20 0.05 10 1100 + +; mode-specific score +i"wobble" 0.10 1.0 0.6 + + + ADDED src/sfx/mode-weapon.csd Index: src/sfx/mode-weapon.csd ================================================================== --- src/sfx/mode-weapon.csd +++ src/sfx/mode-weapon.csd @@ -0,0 +1,23 @@ +; [ʞ] mode-nano.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for activating your weapons + + + + +#include "dqual.inc" +#include "digital.orc" + + + +; common mode switch header +i"chirp" 0.00 0.10 0.10 20 1400 +i"chirp" + 0.20 0.05 10 1100 + +; mode-specific score +i"blare" 0.10 0.60 0.3 +e 0.8 + + + ADDED src/sfx/nano-heal.csd Index: src/sfx/nano-heal.csd ================================================================== --- src/sfx/nano-heal.csd +++ src/sfx/nano-heal.csd @@ -0,0 +1,18 @@ +; [ʞ] nano-heal.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for nanosurgical healing + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"wslope" 0.20 0.4 0.15 15 200 400 550 300 +e 0.65 + + + ADDED src/sfx/nano-shred.n.csd Index: src/sfx/nano-shred.n.csd ================================================================== --- src/sfx/nano-shred.n.csd +++ src/sfx/nano-shred.n.csd @@ -0,0 +1,22 @@ +; [ʞ] nano-shred.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for nanoindustrial shredding + + + + +#include "dqual.inc" +#include "digital.orc" +#include "physical.orc" + + + +//i"keen" 0.00 2.50 0.30 1100 1400 20 +i"keen" 0.00 2.50 0.60 900 1000 15 +i"shred" 0.00 2.50 0.30 +//i"warble" 0.00 2.50 0.03 20 .5 1 1000 2000 +i"warple" 0.00 2.50 0.03 20 .6 .8 1000 1500 900 + + + ADDED src/sfx/nav.csd Index: src/sfx/nav.csd ================================================================== --- src/sfx/nav.csd +++ src/sfx/nav.csd @@ -0,0 +1,18 @@ +; [ʞ] nav.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? navigating UI pages + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"chirp" 0.00 0.13 0.10 20 5000 +e 0.2 + + + ADDED src/sfx/physical.orc Index: src/sfx/physical.orc ================================================================== --- src/sfx/physical.orc +++ src/sfx/physical.orc @@ -0,0 +1,83 @@ +; [ʞ] physical.orc +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? rippy stampy slappy noises for meatspace shenanigans + + +#include "base.orc" + +; for inserting items into slots +instr snap + aw crunch p4, p3, 500, 0.3 + aws stix p4, p3, 200, 0.5 + out aw+aws +endin + +instr slide + anois unirand 1 + avol pulse p3, 0.3 + aw poscil p4*avol, anois * 300 + out aw*p4*avol +endin + +;gisaw ftgen 0, 0, 16384, 10, 0, .2, 0, .4, 0, .6, 0, .8, 0, 1, 0, .8, 0, .6, 0, .4, 0,.2 + +instr zip + kton bpf linseg:k(0,p3,1), 0,300, 0.7, 600, 1, 1000 + ktf unirand 1.0 + kton = kton - ktf * 350 + aw lfo 1.0, kton, 4 + avol fade p3,p4,0.3,0.9 + out aw*avol +endin + +gisine ftgen 0, 0, 16384, 10, 1 + +instr vrrm + avol pulse p3, 0.3 + kvib vibr 1, 100, gisine + aw poscil p4*avol, 300 + (200*kvib) + out aw +endin + +instr flutter + anois unirand 1 + avol pulse p3, 0.3 + aph phasor 20 + out anois*avol*p4*aph +endin + +instr shred_b + avol fade p3,p4, 0.03, 0.97 + ar rand 1.0 + abf poscil 10, 60 + ap poscil 1, abf + af bpf ap, 0,p5, 0.5, p6, 1,p7 + av bpf ap, 0,.1, 0.2,1, 0.5,0.6, 1,.1 + aw reson ar, af, af/2 + out aw*avol*av * 0.01 +endin + +instr shred + al subinstr "shred_b", p4/2, 200, 300, 1500 + ah subinstr "shred_b", p4/2, 600, 800, 1700 + ahh subinstr "shred_b", p4/2, 800, 1200, 1900 + avvh poscil 1, 10 + avvh bpf avvh, 0,0, 0.5,0, 1,1 + avh poscil 1, 10 + avl poscil 1, 5 + out (al*avl) + (ah*avh) +endin + +instr keen + an unirand 1.0 + al bpf an, 0,1000, .5,p5, 1,2000 + ah bpf an, 0,1000, .5,p6, 1,2000 + + ap poscil 1, p7 + at = (al * ap) + (ah * (1-ap)) + + ak poscil p4*0.005, at + + out ak +endin ADDED src/sfx/power-down.csd Index: src/sfx/power-down.csd ================================================================== --- src/sfx/power-down.csd +++ src/sfx/power-down.csd @@ -0,0 +1,20 @@ +; [ʞ] power-down.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for turning off your suit + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"chirp" 0.00 0.40 0.20 20 2200 +i"chirp" + 0.20 0.60 10 950 +;i"blarp" ^+.1 0.3 0.60 +i"spindown" 0.1 1.00 0.50 + + + ADDED src/sfx/power-up.n.csd Index: src/sfx/power-up.n.csd ================================================================== --- src/sfx/power-up.n.csd +++ src/sfx/power-up.n.csd @@ -0,0 +1,19 @@ +; [ʞ] power-up.n.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for turning on your suit + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"warble" 0.00 0.50 0.10 15 .4 .6 400 1200 +i"chirp" 0.70 0.20 0.20 20 1500 +i"chirp" + 0.15 0.20 15 1800 + + + ADDED src/sfx/psi.orc Index: src/sfx/psi.orc ================================================================== --- src/sfx/psi.orc +++ src/sfx/psi.orc @@ -0,0 +1,17 @@ +; [ʞ] psi.orc +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? warpy wacky woobly noises for psionics + +instr wobble + at linseg 0,p3,1 + avol bpf at, \ + 0.00, 0.0, \ + 0.05, 1.0, \ + 0.20, 0.1, + 1.00, 0.0 + aff jspline 1, 40,500 + af bpf aff, 0,400,1,900 + aw poscil avol*p4, af + out aw +endin ADDED src/sfx/success.csd Index: src/sfx/success.csd ================================================================== --- src/sfx/success.csd +++ src/sfx/success.csd @@ -0,0 +1,18 @@ +; [ʞ] success.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? glory hallelujah + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"chirp" 0.00 0.15 0.05 20 1300 +i"chirp" + 0.25 0.10 20 1600 + + + ADDED src/sfx/suit-battery-in.csd Index: src/sfx/suit-battery-in.csd ================================================================== --- src/sfx/suit-battery-in.csd +++ src/sfx/suit-battery-in.csd @@ -0,0 +1,23 @@ +; [ʞ] suit-battery-in.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for inserting a battery into your suit + + + + +#include "dqual.inc" +#include "digital.orc" +#include "physical.orc" + + + +i"snap" 0.00 0.5 0.20 +i"chirp" 0.30 0.20 0.03 23 1600 +i"chirp" ^+0.22 0.10 0.07 20 1900 +i"chirp" ^+0.12 0.10 0.10 20 1300 +s +e 0.1 + + + ADDED src/sfx/suit-chip-in.csd Index: src/sfx/suit-chip-in.csd ================================================================== --- src/sfx/suit-chip-in.csd +++ src/sfx/suit-chip-in.csd @@ -0,0 +1,18 @@ +; [ʞ] suit-chip-in.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effect for inserting a chip into your suit + + + + +#include "dqual.inc" +#include "digital.orc" + + + +i"chirp" 0.00 0.10 0.10 40 1600 +i"chirp" 0.10 0.80 0.05 20 1400 + + + ADDED src/sfx/suit-don.csd Index: src/sfx/suit-don.csd ================================================================== --- src/sfx/suit-don.csd +++ src/sfx/suit-don.csd @@ -0,0 +1,19 @@ +; [ʞ] suit-don.csd +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? strappy slippy zippy noise for pullin on yer spacesuit + + + + +#include "dqual.inc" +#include "physical.orc" + + + +i"slide" 0.00 0.50 0.60 +i"zip" 0.40 0.50 0.03 +e 2 + + + ADDED src/sfx/violence.orc Index: src/sfx/violence.orc ================================================================== --- src/sfx/violence.orc +++ src/sfx/violence.orc @@ -0,0 +1,10 @@ +; [ʞ] violence.orc +; ~ lexi hale +; 🄯 CC-NC-BY-SA 3.0 +; ? sound effects for the many exciting +; ways of doing violence to people + +instr shot + aw crunch p4, p3, 350, 0.95 + out aw +endin ADDED starsoul.ct Index: starsoul.ct ================================================================== --- starsoul.ct +++ starsoul.ct @@ -0,0 +1,77 @@ +# starsoul +[*starsoul] is a sci-fi survival game. you play the survivor of a disaster in space, stranded on a just-barely-habitable world with nothing but your escape pod, your survival suit, and some handy new psionic powers that the accident seems to have unlocked in you. scavenge resources, search alien ruins, and build a base where you can survive indefinitely, but be careful: winter approaches, and your starting suit heater is already struggling to keep you alive in the comparatively warm days of "summer". + +## story +about a month ago, you woke up to unexpected good news. your application to join the new Commune colony at Thousand Petal, submitted in a moment of utter existential weariness and almost in jest, was actually accepted. your skillset was a "perfect match" for the budding colony's needs, claimed the Population Control Authority, and you'd earned yourself a free trip to your new home -- on a swanky state transport, no less. + +it took a few discreet threats and bribes from Commune diplomats, but after a week of wrangling with the surly Crown Service for Comings & Goings -- whose bureaucrats seemed outright [!offended] that you actually managed to find a way off that hateful rock -- you secured grudging clearance to depart. you celebrated by telling your slackjawed boss exactly what you thought of him in a meeting room packed with his fellow parasites -- and left Circumsolar Megascale with a new appreciation for the value of your labor, as they found themselves desperately scrabbling for a replacement on short notice. + +you almost couldn't believe it when the Commune ship -- a sleek, solid piece of engineering whose graceful descent onto the landing pad seemed to sneer at the lurching rattletraps arrayed all around it -- actually showed up. in a daze you handed over your worldly possessions -- all three of them -- to a valet with impeccable manners, and climbed up out of the wagie nightmare into high orbit around your homeworld. the mercenary psion aboard, a preening Usukwinti with her very own luxury suite, tore a bleeding hole in the spacetime metric, and five hundred hopeful souls dove through towards fancied salvation. "sure," you thought to yourself as you slipped into your sleek new nanotech environment suit, itself worth more than the sum total of your earnings on Flame of Unyielding Purification, "life won't be easy -- but damn it, it'll [!mean] something out there." + +a free life on the wild frontier with a nation of comrades to have your back, with the best tech humans can make, fresh, clean water that isn't laced with compliance serum, and -- best of all -- never having to worry about paying rent again. it was too good to be true, you mused. + +clearly, the terrorists who blew up your ship agreed. + +you're still not certain what happened. all you know for sure is that transport was carrying more than just people. in those last hectic moments, you caught a glimpse of something -- maybe machine, maybe artwork, and [!definitely] ancient beyond measure. you've seen artifacts before in museums, of course; in fact, thanks to a childhood fascination, you can still name all the Elder Races and the Forevanished Ones off the top of your head. + +you have no [!idea] what that [!thing] was or who in the sublimated [!fuck] could possibly have made it. + +but one thing is for certain: your ship wasn't the only thing it ripped open when it blew. because when you woke up in your tiny escape pod beyond the furthest edge of the Reach, circling Farthest Shadow in a suicide orbit, you discovered yourself transformed into something impossible. a contradiction in terms. + +a human psionic. + +for years beyond counting, the Starsouled species -- three of whom yet live and deign once every so often to notice the Lesser Races -- have held the galaxy in sway through their monopoly on psionic power. of all the Thinking Few, only they are free to wander the distant stars at whim, heedless of the lightspeed barrier. there are no mechanisms for FTL travel or reactionless drive without that innate power, and, they assured us, psionic channels are fixed in the soul. your species either has the power or it doesn't. + +[!liars], all of them. + +are there other survivors? have they been similarly changed? what was that artifact and who were those terrorists? important questions, all, but they pale in comparison with the most important one: + +how the fuck are you going to survive the next 24 hours? + +## engine +starsoul is developed against a bleeding-edge version of minetest. it definitely won't work with anything older than v5.7, and ideally you should build directly from master. + +starsoul is best used with a patched version of minetest, though it is compatible with vanilla. the recommended patches are: + +* [>p11143 11143] - fix third-person view orientation + + p11143: https://github.com/minetest/minetest/pull/11143.diff + +### shadows +i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starsouled player character meshes. for the sake of those who don't mind these glitches, Starsoul does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature. + +## gameplay +starsoul is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety. + +### controls +summon your Suit Interface by pressing the [*E] / [*Inventory] key. this will allow you to move items around in your inventory, but more importantly, it also allows you select or configure your Interaction Mode. + +the top three buttons can be used to select (or deactivate) an Interaction Mode. an Interaction Mode can be configured by pressing the button immediately below. the active Interaction Mode controls the behavior of the mouse buttons when no item is selected in the hotbar. + +the modes are: + * [*Fabrication]: use your suit's onboard nanotech to directly manipulate matter in the world. + ** [*Left Button] / [*Punch]: activate your primary nano program. by default this activates your nanoshredder, reducing the targeted object to monatomic powder and storing the resulting elements in your suit for use with the Matter Compiler + ** [*Right Button] / [*Place]: activate your secondary nano program. by default, if your suit compiler can generate sufficiently large objects, creates a block of the configured type directly in the world without having to build it by hand + * [*Psionics]: wield the awesome, if illicitly obtained, power of mind over matter + ** [*Left Button] / [*Punch]: perform your selected Primary Power + ** [*Right Button] / [*Place]: perform your selected Secondary Power + * [*Weapon]: military-grade suits have built-in hardpoints for specialized weapon systems that draw directly on your suit battery for power (and in the most exotic cases, your psi reserve) + ** [*Left Button] / [*Punch]: fire your primary weapon + ** [*Right Button] / [*Place]: fire your offhand weapon / summon your shield + +to use a tool, select it in the hotbar. even if an Interaction Mode is active, the tool will take priority. press [*Left Button] / [*Punch] to use the tool on a block; for instance, to break a stone with a jackhammer. to configure a tool or use its secondary functions, if any, press [*Right Button] / [*Place]. + +hold [*Aux1] to activate your selected Maneuver. by default this is Sprint, which will consume stamina to allow you to run much faster. certain suits offer the Flight ability, which allows slow, mid-range flight. you can also unlock the psionic ability Lift, which allows very rapid flight but consumes psi at a prodigious rate. + +you can only have one Maneuver active at a time, whether this is a Psi Maneuver (consuming psi), a Suit Maneuver (consuming battery), or a Body Maneuver (consuming stamina). Maneuvers are activated in their respective panel. + +### psionics +there are four types of psionic abilities: Manual, Maneuver, Ritual, and Contextual. + +you can assign two Manual abilities at any given time and access them with the mouse buttons in Psionics mode. + +you can select a Psi Maneuver in the Psionics panel and activate it by holding [*Aux1]. + +a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starsouled astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers. + +a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starsouled practitioners maintain their own Psionics Lab.