@@ -74,16 +74,204 @@ {'default:copper_ingot', 'sorcery:electrumblock', 'default:copper_ingot'}; {'default:copper_ingot', 'default:copper_ingot', 'default:copper_ingot'}; }; }; -minetest.register_craft { - output = 'sorcery:wire 4'; - recipe = { - {'', 'basic_materials:copper_wire',''}; - {'', 'sorcery:fragment_electrum', ''}; - {'', 'basic_materials:copper_wire',''}; + +local makeswitch = function(switch, desc, tex, tiles, power) + for _,active in pairs{true,false} do + local turn = function(pos) + local n = minetest.get_node(pos) + minetest.sound_play('doors_steel_door_open', { + gain = 0.7; + pos = pos; + }, true) + local leymap = active and sorcery.ley.mapnet(pos) or nil + minetest.swap_node(pos, { + name = active and (switch .. '_off') + or switch; + param1 = n.param1; + param2 = n.param2; + }) + if active then + -- if we're turning it off, use the old map, + -- because post-swap the network will be + -- broken and notify won't reach everyone + leymap.map[leymap.startpos] = nil + sorcery.ley.notifymap(leymap.map) + else sorcery.ley.notify(pos) end + end + local tl = table.copy(tiles) + tl[6] = tex .. '^sorcery_ley_switch_panel.png^sorcery_ley_switch_' .. (active and 'down' or 'up') .. '.png'; + minetest.register_node(switch .. (active and '' or '_off'), { + description = desc; + drop = switch; + tiles = tl; + paramtype2 = 'facedir'; + groups = { + cracky = 2; choppy = 1; + punch_operable = 1; + sorcery_ley_device = active and 1 or 0; + }; + _sorcery = { + ley = active and { + mode = 'signal'; power = power; + } or nil; + }; + on_punch = function(pos,node,puncher,point) + if puncher ~= nil then + if puncher:get_wielded_item():is_empty() then + turn(pos) + end + end + return minetest.node_punch(pos,node,puncher,point) + end; + on_rightclick = turn; + }) + end +end + +for _,b in pairs { + {'Applewood', 'wood', 'default_wood.png'}; + {'Junglewood', 'junglewood', 'default_junglewood.png'}; + {'Pine', 'pine_wood', 'default_pine_wood.png'}; + {'Acacia', 'acacia_wood', 'default_pine_wood.png'}; + {'Aspen', 'aspen_wood', 'default_aspen_wood.png'}; + {'Stone', 'stone', 'default_stone.png'}; + {'Cobblestone', 'cobble', 'default_cobble.png'}; + {'Stone Brick', 'stonebrick', 'default_stone_brick.png'}; + {'Brick', 'brick', 'default_brick.png'}; +} do + local id = 'sorcery:conduit_half_' .. b[2] + local switch = 'sorcery:conduit_switch_' .. b[2] + local item = (b[4] or 'default') .. ':' .. b[2] + local tex = b[3] + local mod = '^[lowpart:50:' + local sidemod = '^[transformR270^[lowpart:50:' + local unflip = '^[transformR90' + local tiles = { + 'sorcery_conduit_copper_top.png'..mod..tex; -- top + tex..mod..'sorcery_conduit_copper_top.png'; + tex .. sidemod .. 'sorcery_conduit_copper_side.png' .. unflip; -- side + 'sorcery_conduit_copper_side.png' .. sidemod .. tex .. unflip; -- side + 'sorcery_conduit_copper_side.png'; -- back + tex; -- front + } + minetest.register_node(id, { + description = 'Half-' .. b[1] .. ' Conduit'; + paramtype2 = 'facedir'; + groups = { + cracky = 2; + choppy = 1; + sorcery_ley_device = 1; + sorcery_ley_conduit = 1; + }; + _sorcery = { + ley = { mode = 'signal'; power = 5; } + }; + tiles = tiles; + }) + minetest.register_craft { + output = id .. ' 4'; + recipe = { + {item, 'sorcery:conduit'}; + {item, 'sorcery:conduit'}; + }; + }; + makeswitch(switch, b[1] .. " Conduit Switch", tex, tiles, 5) + minetest.register_craft { + output = switch; + recipe = { + {'xdecor:lever_off',id}; + }; } -}; +end +makeswitch('sorcery:conduit_switch', "Conduit Switch", 'sorcery_conduit_copper_side.png', { + 'sorcery_conduit_copper_top.png'; + 'sorcery_conduit_copper_top.png'; + 'sorcery_conduit_copper_side.png'; + 'sorcery_conduit_copper_side.png'; + 'sorcery_conduit_copper_side.png'; + 'sorcery_conduit_copper_side.png'; +}, 10) +minetest.register_craft { + output = 'sorcery:conduit_switch'; + recipe = { + {'xdecor:lever_off','sorcery:conduit'}; + }; +} + +for name,metal in pairs(sorcery.data.metals) do + if metal.conduct then + local cable = 'sorcery:cable_' .. name + minetest.register_node(cable, { + description = sorcery.lib.str.capitalize(name) .. " Cable"; + drawtype = 'nodebox'; + groups = { + sorcery_ley_device = 1; snappy = 3; attached = 1; + sorcery_ley_cable = 1; + }; + _sorcery = { + ley = { mode = 'signal', power = metal.conduct }; + }; + sunlight_propagates = true; + node_box = { + type = 'connected'; + disconnected = { -0.05, -0.35, -0.40; 0.05, -0.25, 0.40 }; + connect_front = { -0.05, -0.35, -0.50; 0.05, -0.25, 0.05 }; + connect_back = { -0.05, -0.35, -0.05; 0.05, -0.25, 0.50 }; + connect_right = { -0.05, -0.35, -0.05; 0.50, -0.25, 0.05 }; + connect_left = { -0.50, -0.35, -0.05; 0.05, -0.25, 0.05 }; + connect_top = { -0.05, -0.25, -0.05; 0.05, 0.50, 0.05 }; + connect_bottom = { -0.05, -0.50, -0.05; 0.05, -0.35, 0.05 }; + }; + connects_to = { 'group:sorcery_ley_device', 'default:mese' }; + -- harcoding mese is kind of cheating -- figure out a + -- better way to do this for the longterm + paramtype = 'light'; + -- paramtype2 = 'facedir'; + after_place_node = function(pos, placer, stack, point) + local vec = vector.subtract(point.under, pos) + local n = minetest.get_node(pos) + n.param2 = minetest.dir_to_facedir(vec) + minetest.swap_node(pos,n) + end; + tiles = { 'sorcery_ley_plug.png' }; + }) + + minetest.register_craft { + output = cable .. ' 8'; + recipe = { + {'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'}; + { metal.parts.fragment, metal.parts.fragment, metal.parts.fragment }; + {'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'}; + }; + replacements = { + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + {'basic_materials:copper_wire', 'basic_materials:empty_spool'}; + }; + }; + end +end + +-- ley.notify will normally be called automatically, but if a +-- ley-producer or consume has fluctuating levels of energy +-- consumption, it should call this function when levels change +sorcery.ley.notifymap = function(map) + for pos,name in pairs(map) do + local props = minetest.registered_nodes[name]._sorcery + if props and props.on_leychange then + props.on_leychange(pos) + end + end +end +sorcery.ley.notify = function(pos) + local n = sorcery.ley.mapnet(pos) + sorcery.ley.notifymap(n.map) +end sorcery.ley.field_to_current = function(strength,time) local ley_factor = 0.25 -- a ley harvester will produce this much current with @@ -105,8 +293,9 @@ }; minetest.register_node('sorcery:condenser', { description = 'Condenser'; drawtype = 'mesh'; + paramtype2 = 'facedir'; mesh = 'sorcery-condenser.obj'; selection_box = box; collision_box = box; tiles = { @@ -125,16 +314,16 @@ local meta = minetest.get_meta(pos) meta:set_string('infotext','Condenser') end; _sorcery = { - ley = { mode = 'produce' }; - on_leycalc = function(pos,time) - local l = sorcery.ley.estimate(pos) - return { - power = sorcery.ley.field_to_current(l.force, time); - affinity = l.aff; - } - end; + ley = { mode = 'produce'; + power = function(pos,time) + return sorcery.ley.field_to_current(sorcery.ley.estimate(pos).force, time); + end; + affinity = function(pos) + return sorcery.ley.estimate(pos).aff + end; + }; }; }) end @@ -144,12 +333,21 @@ {'sorcery:accumulator'}; {'sorcery:conduit'}; }; } +sorcery.ley.txofs = { + {x = 0, z = 0, y = 0}; + {x = -1, z = 0, y = 0}; + {x = 1, z = 0, y = 0}; + {x = 0, z = -1, y = 0}; + {x = 0, z = 1, y = 0}; + {x = 0, z = 0, y = -1}; + {x = 0, z = 0, y = 1}; +} sorcery.ley.mapnet = function(startpos,power) -- this function returns a list of all the nodes accessible from -- a ley network and their associated positions - local net = {} + local net,checked = {},{} power = power or 0 local devices = { consume = {}; @@ -158,10 +356,11 @@ } local numfound = 0 local maxconduct = 0 local minconduct + local startkey local foundp = function(p) - for k in pairs(net) do + for _,k in pairs(checked) do if vector.equals(p,k) then return true end end return false end @@ -170,29 +369,28 @@ -- replace it with a linear one at some point local function find(positions) local searchnext = {} for _,pos in pairs(positions) do - for _,p in pairs { - {x = 0, z = 0, y = 0}; - {x = -1, z = 0, y = 0}; - {x = 1, z = 0, y = 0}; - {x = 0, z = -1, y = 0}; - {x = 0, z = 1, y = 0}; - {x = 0, z = 0, y = -1}; - {x = 0, z = 0, y = 1}; - } do local sum = vector.add(pos,p) + for _,p in pairs(sorcery.ley.txofs) do + local sum = vector.add(pos,p) if not foundp(sum) then + checked[#checked + 1] = sum local nodename = minetest.get_node(sum).name + if nodename == 'ignore' then + minetest.load_area(sum) + nodename = minetest.get_node(sum).name + end if minetest.get_item_group(nodename,'sorcery_ley_device') ~= 0 or sorcery.data.compat.ley[nodename] then - local d = sorcery.ley.sample(pos,1,nodename) + local d = sorcery.ley.sample(pos,1,nodename,{query={mode=true}}) assert(d.mode == 'signal' or d.mode == 'consume' or d.mode == 'produce') devices[d.mode][#(devices[d.mode]) + 1] = { id = nodename; pos = sum; } if d.mode == 'signal' then + d.power = sorcery.ley.sample(pos,1,nodename,{query={power=true}}).power if d.power > power then if minconduct then if d.power < minconduct then minconduct = d.power @@ -204,8 +402,13 @@ end end numfound = numfound + 1; net[sum] = nodename; + if not startkey then + if vector.equals(startpos,sum) then + startkey = sum + end + end searchnext[#searchnext + 1] = sum; end end end @@ -219,8 +422,9 @@ return { count = numfound; map = net; devices = devices; + startpos = startkey; conduct = { min = minconduct; max = maxconduct; }; @@ -244,9 +448,10 @@ for i=1,#modetbl do modetbl[modetbl[i]] = i end local m = sorcery.lib.marshal local enc, dec = m.transcoder { mode = m.t.u8; - power = m.t.u32; -- power generated/consumed * 10,000 + minpower = m.t.u32; -- min power generated/consumed * 10,000 + maxpower = m.t.u32; -- max power generated/consumed * 10,000 affinity = m.g.array(m.t.u8); -- indexes into afftbl } sorcery.ley.encode = function(l) local idxs = {} @@ -254,9 +459,10 @@ idxs[#idxs+1] = afftbl[k] end return meta_armor(enc { mode = modetbl[l.mode]; - power = l.power * 10000; + minpower = l.minpower * 10000; + maxpower = l.maxpower * 10000; affinity = idxs; }, true) end sorcery.ley.decode = function(str) @@ -266,9 +472,11 @@ affs[#affs+1] = afftbl[k] end return { mode = modetbl[obj.mode]; - power = obj.power / 10000.0; + minpower = obj.minpower / 10000.0; + maxpower = obj.maxpower / 10000.0; + power = (obj.minpower == obj.maxpower) and obj.minpower or nil; affinity = affs; } end end @@ -276,83 +484,308 @@ local meta = minetest.get_node(pos) meta:set_string('sorcery:ley',sorcery.ley.encode(l)) end -sorcery.ley.sample = function(pos,timespan,name) +sorcery.ley.sample = function(pos,timespan,name,flags) -- returns how much ley-force can be transmitted by a -- device over timespan + local ret = {} name = name or minetest.get_node(pos).name + flags = flags or {} + flags.query = flags.query or { + mode = true; power = true; affinity = true; + minpower = true; maxpower = true; + } local props = minetest.registered_nodes[name]._sorcery - local callback = props and props.on_leycalc or nil - local p,a,m - if callback then - local gen = callback(pos,timespan) - p = gen.power - a = gen.affinity - m = gen.mode + + local evaluate = function(v) + if type(v) == 'function' then + return v(pos) + else return v end end - if not (p and a and m) then + local leymeta do local nm = minetest.get_meta(pos) if nm:contains('sorcery:ley') then - local l = sorcery.ley.decode(nm:get_string('sorcery:ley')) - p = p or sorcery.ley.field_to_current(l.power,timespan) - a = a or l.affinity - m = m or l.mode + leymeta = sorcery.ley.decode(nm:get_string('sorcery:ley')) + end + end + + local compat = sorcery.data.compat.ley[name] + + local lookup = function(k,default) + if leymeta and leymeta[k] then return leymeta[k] + elseif props and props.ley and props.ley[k] then return props.ley[k] + elseif compat and compat[k] then return compat[k] + else return default end + end + if flags.query.mode then ret.mode = evaluate(lookup('mode','none')) end + if flags.query.affinity then ret.affinity = evaluate(lookup('affinity',{})) end + if flags.query.minpower or flags.query.maxpower or flags.query.power then + local condset = function(name,var) + if flags.query[name] then ret[name] = var end + end + local p = lookup('power') + if p then + if type(p) == 'function' then + -- we have a single function to calculate power usage; we need to + -- check whether it returns min,max or a constant + local min, max = p(pos,timespan) + if (not max) or min == max then + ret.power = min + condset('power',min) + condset('minpower',min) + condset('maxpower',min) + else + condset('minpower',min) + condset('maxpower',max) + end + else -- power usage is simply a constant + condset('power',p) + condset('minpower',p) + condset('maxpower',p) + end + else + local feval = function(v) + if type(v) == 'function' then + return v(pos,timespan) + else return v * timespan end + end + local min = feval(lookup('minpower')) + local max = feval(lookup('maxpower')) + condset('minpower',min) + condset('maxpower',max) + if min == max then condset('power',min) end end end - if (not (p and a and m)) and props and props.ley then - p = p or sorcery.ley.field_to_current(props.ley.power,timespan) - a = a or props.ley.affinity - m = m or props.ley.mode + if ret.power then + if flags.query.minpower and not ret.minpower then ret.minpower = power end + if flags.query.maxpower and not ret.maxpower then ret.maxpower = power end end - - if (not (p and a and m)) then - local compat = sorcery.data.compat.ley[name] - if compat then - p = p or sorcery.ley.field_to_current(compat.power,timespan) - a = a or compat.affinity - m = m or compat.mode - end - end - - return { - power = p or 0; - mode = m or 'none'; - affinity = a or {}; - } + return ret end sorcery.ley.netcaps = function(pos,timespan,exclude) local net = sorcery.ley.mapnet(pos) local maxpower = 0 local freepower = 0 local affs,usedaffs = {},{} + local flexpowerdevs = {} + local devself for _,n in pairs(net.devices.produce) do + if vector.equals(pos,n.pos) then devself = n end if not exclude or not vector.equals(n.pos,exclude) then local ln = sorcery.ley.sample(n.pos,timespan,n.id) + n.powersupply = ln.power + n.affinity = ln.affinity maxpower = maxpower + ln.power + -- production power does not vary, tho at some point it + -- might be useful to enable some kind of power scaling for _,a in pairs(ln.affinity) do affs[a] = (affs[a] or 0) + 1 end end end freepower = maxpower; for _,n in pairs(net.devices.consume) do + if vector.equals(pos,n.pos) then devself = n end if not exclude or not vector.equals(n.pos,exclude) then - local ln = sorcery.ley.sample(n.pos,timespan,n.id) - freepower = freepower - ln.power + local ln = sorcery.ley.sample(n.pos,timespan,n.id, { + query = { power = true; minpower = true; maxpower = true; affinity = true; }; + }) + n.powerdraw = (ln.minpower <= freepower) and ln.minpower or 0 + freepower = freepower - n.powerdraw + -- merge in sample data and return it along with the map + n.minpower = ln.minpower + n.maxpower = ln.maxpower + n.affinity = ln.affinity + if ln.maxpower > ln.minpower then + flexpowerdevs[#flexpowerdevs+1] = n + end for _,a in pairs(ln.affinity) do usedaffs[a] = (usedaffs[a] or 0) + 1 end end end + + -- now we know the following: all devices; if possible, have been + -- given the minimum amount of power they need to run. if freepower + -- < 0 then the network is overloaded and inoperable. if freepower>0, + -- we now need to distribute the remaining power to devices that + -- have a variable power consumption. there's no clean way of doing + -- this, so we use the following algorithm: + -- 1. take a list of devices that want more power + -- 2. divide the amount of free power by the number of such devices + -- to derive the maximum power that can be allocated to any device + -- 3. iterate through the devices. increase their power consumption by + -- the maximum term. any device that is satiated can be removed from + -- the list. + -- 4. if there is still power remaining, repeat until there is not. + + while freepower > 0 and #flexpowerdevs > 0 do + local nextiter = {} + local maxgive = freepower / #flexpowerdevs + for _,d in pairs(flexpowerdevs) do + local give = math.min(maxgive,d.maxpower - d.powerdraw) + freepower = freepower - give + d.powerdraw = d.powerdraw + give + if d.powerdraw < d.maxpower then + nextiter[#nextiter+1] = d + end + end + flexpowerdevs = nextiter + end return { net = net; freepower = freepower; maxpower = maxpower; affinity = affs; affinity_balance = usedaffs; + self = devself; } end + +minetest.register_on_placenode(function(pos, node) + if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then + sorcery.ley.notify(pos) + end +end) + +local constants = { + generator_max_energy_output = 5; + -- how much energy a generator makes after + + generator_time_to_max_energy = 150; + -- seconds of activity + + generator_power_drain_speed = 0.1; + -- points of energy output drained per second of no fuel +} +local update_generator = function(pos) + minetest.get_node_timer(pos):start(1) +end +local generator_update_formspec = function(pos) + local meta = minetest.get_meta(pos) + local burnprog = math.min(1,meta:get_float('burnleft') / meta:get_float('burntime')) + local power = meta:get_float('power') + local inv = meta:get_inventory() + local lamps = '' + for i=0,4 do + local color + if power - i >= 1 then + color = 'red' + elseif power - i > 0 then + color = 'yellow' + else + color = 'off' + end + lamps = lamps .. string.format([[ + image[%f,0.5;1,1;sorcery_statlamp_%s.png] + ]], 2.5 + i, color) + end + meta:set_string('formspec', string.format([[ + size[8,5.8] + list[context;fuel;0.5,0.5;1,1] + list[current_player;main;0,2;8,4] + image[1.5,0.5;1,1;default_furnace_fire_bg.png^[lowpart:%u%%:default_furnace_fire_fg.png] + ]], math.floor(burnprog * 100)) .. lamps) +end +for _,active in pairs{true,false} do + local id = 'sorcery:generator' .. (active and '_active' or '') + minetest.register_node(id, { + description = 'Generator'; + paramtype2 = 'facedir'; + groups = { cracky = 2; sorcery_ley_device = 1; }; + drop = 'sorcery:generator'; + tiles = { + 'sorcery_ley_generator_top.png'; + 'sorcery_ley_generator_bottom.png'; + 'sorcery_ley_generator_side.png'; + 'sorcery_ley_generator_side.png'; + 'sorcery_ley_generator_back.png'; + 'sorcery_ley_generator_front_' .. (active and 'on' or 'off') .. '.png'; + }; + on_construct = function(pos) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + meta:set_string('infotext','Generator') + meta:set_float('burntime',0) + meta:set_float('burnleft',0) + meta:set_float('power',0) + generator_update_formspec(pos) + inv:set_size('fuel',1) + end; + after_dig_node = sorcery.lib.node.purge_container; + on_metadata_inventory_put = update_generator; + on_metadata_inventory_take = update_generator; + on_timer = function(pos,delta) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local self = minetest.get_node(pos) + local timeleft = meta:get_float('burnleft') - delta + local again = false + local power = meta:get_float('power') + local burning = active + if timeleft < 0 then timeleft = 0 end + if not active or timeleft == 0 then + if inv:is_empty('fuel') then + -- no fuel, can't start/keep going. drain power if + -- necessary, otherwise bail + burning = false + if power > 0 then + power = math.max(0, power - constants.generator_power_drain_speed) + again = true + end + else + -- fuel is present, let's burn it + local res,decin = minetest.get_craft_result { + method = 'fuel'; + items = {inv:get_stack('fuel',1)}; + } + meta:set_float('burntime',res.time) + timeleft = res.time + inv:set_stack('fuel',1,decin.items[1]) + again = true + burning = true + end + else + local eps = constants.generator_max_energy_output / constants.generator_time_to_max_energy + power = math.min(constants.generator_max_energy_output, power + eps*delta) + again = true + end + ::stop:: meta:set_float('power',power) + meta:set_float('burnleft',timeleft) + generator_update_formspec(pos) + if burning and not active then + minetest.swap_node(pos, { + name = 'sorcery:generator_active'; + param1 = self.param1, param2 = self.param2; + }) + elseif active and not burning then + minetest.swap_node(pos, { + name = 'sorcery:generator'; + param1 = self.param1, param2 = self.param2; + }) + end + return again + end; + allow_metadata_inventory_put = function(pos,listname,index,stack,user) + local res = minetest.get_craft_result { + method = 'fuel'; + items = {stack}; + } + if res.time ~= 0 then return stack:get_count() + else return 0 end + end; + _sorcery = { + ley = { + mode = 'produce', affinity = {'praxic'}; + power = function(pos,delta) + local meta = minetest.get_meta(pos) + return meta:get_float('power') * delta; + end; + }; + }; + }) +end