sorcery  wands.lua at [119981d2d8]

File wands.lua artifact 110b3cfc3f part of check-in 119981d2d8


-- magic wands

-- first we need to go through and register all the
-- possible base wands. a base is a three-tuple of
-- (wood, maybe wire, maybe gem). wood determines base
-- affinity; wire alters how the wand uses energy

local u = sorcery.lib

sorcery.wands = {
	kinds = {};
	materials = {
		wood = {
			pine = {
				tex = u.image('default_pine_wood.png');
			};
			apple = {
				tex = u.image('default_wood.png');
				tree = 'default:tree';
				plank = 'default:wood';
			};
			acacia = {
				tex = u.image('default_acacia_wood.png');
			};
			aspen = {
				tex = u.image('default_aspen_wood.png');
			};
			jungle = {
				tex = u.image('default_junglewood.png');
				tree = 'default:jungletree';
				plank = 'default:junglewood';
			};
		};
		core = {
			obsidian = {
				item = 'default:obsidian_shard';
				wandprops = { sturdiness = 1.5 };
			};
			diamond = {
				item = 'sorcery:shard_diamond';
				wandprops = { sturdiness = 1.7, reliability = 0.85 };
			};
			mese = {
				item = 'default:mese_crystal_fragment';
				wandprops = { generate = 2 };
			};
			cobalt = {
				item = 'sorcery:powder_cobalt';
				wandprops = { power = 1.4 };
			};
			iridium = {
				item = 'sorcery:powder_iridium';
				wandprops = { sturdiness = 1.25, power = 1.25 };
			};
			lithium = {
				item = 'sorcery:powder_lithium';
				wandprops = { power = 1.7, reliability = 0.70 };
			};
			gold = {
				item = 'sorcery:powder_gold';
				wandprops = { duration = 1.3, power = 1.3 };
			};
			tungsten = {
				item = 'sorcery:powder_tungsten';
				wandprops = { duration = 1.7, sturdiness = 1.25 };
			};
			platinum = {
				item = 'sorcery:powder_platinum';
				wandprops = { bond = 1; }
			};
			vidrium = {
				item = 'sorcery:powder_vidrium';
				wandprops = { bond = 2; }
			};
			impervium = {
				item = 'sorcery:powder_impervium';
				wandprops = { bond = 1, power = 1.5 };
			};
		};
		wire = {
			gold = {
				-- gold limits the amount of wear-and-tear on
				-- the wand, but causes the power to weaken as
				-- it empties
				tone = u.color(255,255,59);
				tex = u.image('default_gold_block.png');
				wandprops = { sturdiness = 1.2, weaken = 1 };
			};
			copper = {
				-- copper causes the wand to recharge more quickly
				-- but power levels are unpredictable
				tone = u.color(255,117,40);
				tex = u.image('default_copper_block.png');
				wandprops = { flux = 0.7, chargetime = 0.5 };
			};
			silver = {
				tone = u.color(215,238,241);
				tex = u.image('default_gold_block.png'):colorize(u.color(255,238,241), 255);
				wandprops = {};
			};
			steel = {
				tone = u.color(255,255,255);
				tex = u.image('default_steel_block.png');
				wandprops = {};
			};
		};
		gem = sorcery.data.gems;
	};
	util = {
		baseid = function(wand)
			local elts = {wand.wood}
			if wand.gem  ~= nil then elts[#elts + 1] = wand.gem end
			if wand.wire ~= nil then elts[#elts + 1] = wand.wire end
			return 'sorcery:wand_' .. table.concat(elts,'_')
		end;
		basename = function(wand,full)
			local name = wand.wood .. 'wood wand'
			if wand.core ~= nil then name = wand.core .. '-core ' .. name end
			if full then
				if wand.gem  ~= nil then name = wand.gem .. ' ' .. name end
				if wand.wire ~= nil then name = wand.wire .. 'clad ' .. name end
			end
			return u.str.capitalize(name)
		end;
		fullname = function(stack)
			local proto = sorcery.wands.util.getproto(stack)
			local wm = stack:get_meta()
			local spell = wm:get_string('sorcery_wand_spell')
			if spell ~= '' then
				local sd = sorcery.data.spells[spell]
				return u.str.capitalize(sd.name) .. ' wand';
			else
				return sorcery.wands.util.basename(proto)
			end
		end;
		wear = function(item)
			local meta = item:get_meta()
			local spell = meta:get_string('sorcery_wand_spell')
			local proto = sorcery.wands.util.getproto(item)
			if spell == "" then return 0 end

			local uses = sorcery.data.spells[spell].uses 

			if proto.core then
				local core = sorcery.wands.materials.core[proto.core]
				if not core then goto nocore end

				if core.sturdiness then uses = uses * core.sturdiness end
			::nocore::end

			return 65536 / uses
		end;
		getproto = function(stack)
			local proto = stack:get_definition()._proto
			local core = stack:get_meta():get_string('sorcery_wand_core')
			if core ~= '' then
				proto = table.copy(proto)
				proto.core = core
			end
			return proto
		end;
		matprops = function(proto)
			local matprops = {}
			for k,v in pairs(proto) do
				if sorcery.wands.materials[k] then
					local mp = sorcery.wands.materials[k][v].wandprops
					if mp then
						matprops = sorcery.lib.tbl.deepmerge(matprops, mp,
							function(a,b,key)
								if key == 'bond'
									then return a+b
									else return a*b
								end
							end)
					end
				end
			end
			return matprops
		end;
		basedesc = function(wand)
			local desc = 'wand fashioned from the wood of the ' .. wand.wood .. ' tree'
			if wand.gem ~= nil then
				desc = wand.gem .. '-tipped ' .. desc
			end
			if wand.wire ~= nil then
				desc = desc .. ', clad in ' .. wand.wire
			end
			if wand.core ~= nil then
				desc = desc .. ', with a core of ' .. wand.core
			end
			if u.tbl.has({'a', 'e', 'i', 'o', 'u'}, string.sub(desc, 1,1)) then
				return 'An ' .. desc
			else
				return 'A ' .. desc
			end
		end;
	};
}

-- this feels extremely lazy but whatever
local treetoplank, planktotree = {}, {}
for k,v in pairs(sorcery.wands.materials.wood) do
	local plank = v.plank or 'default:' .. k .. '_wood'
	local tree = v.tree or 'default:' .. k .. '_tree'
	treetoplank[tree] = plank
	planktotree[plank] = tree
end

local wand_cast = function(stack, user, target)
	local meta = stack:get_meta()
	local wand = sorcery.wands.util.getproto(stack)
	if meta:contains('sorcery_wand_spell') == false then return nil end
	local spell = meta:get_string('sorcery_wand_spell')
	local spelldata = sorcery.data.spells[spell]

	-- wands don't work in anti-magic fields
	local probe = sorcery.spell.probe(user:get_pos())
	if probe.disjunction and not spelldata.ignore_disjunction then return nil end

	local castfn = spelldata.cast
	if castfn == nil then return nil end
	local matprops = sorcery.wands.util.matprops(wand)
	if matprops.bond then
		local userct, found = 0, false
		for i=1,matprops.bond do
			local prop = 'bound_user_' .. tostring(i)
			if meta:contains(prop) then
				userct = i
				local name = meta:get_string(prop)
				-- print('wand bound to',name,i)
				if name == user:get_player_name() then found = true break end
			else break end
		end
		
		if not found then
			if userct < matprops.bond then
				-- print('binding wand to caster')
				minetest.sound_play("xdecor_enchanting", { --FIXME make own sounds
					pos = user:get_pos();
					gain = 0.8;
				})
				sorcery.vfx.cast_sparkle(user, sorcery.lib.color(25,129,255), 2)
				meta:set_string('bound_user_' .. tostring(userct+1), user:get_player_name())
				return stack
			else
				-- user not in table AND no binding slots left. UH OH
				sorcery.vfx.cast_sparkle(user, sorcery.lib.color(255,0,0), 4)
				sorcery.vfx.bloodburst(user:get_pos())
				user:punch(user, 1.0, {
					full_punch_interval = 1.0;
					damage_groups = { fleshy = math.random(1,5) };
				}, nil)
				return stack
			end
		end
	end

	local uprops = user:get_properties();
	local context = {
		base = wand;
		stats = matprops;
		meta = meta;
		item = stack;
		caster = user;
		target = target;
		probe = probe;
		today = minetest.get_day_count();
		heading = {
			pos   = user:get_pos();
			yaw   = user:get_look_dir();
			pitch = user:get_look_vertical();
			angle = user:get_look_horizontal();
			eyeheight = uprops.eye_height;
		};
		wearmult = 1;
	}
	local result = castfn(context)
	if result ~= false then
		minetest.sound_play(sorcery.data.spells[spell].sound or "sorcery_chime", { --FIXME make better sound
			pos = user:get_pos();
			gain = 0.8;
		})
		-- minetest.add_particle {
		-- 	pos = vector.add(vector.add(user:get_pos(), vector.multiply(user:get_look_dir(),1.1)), {y=1.6,z=0,x=0});
		-- 	velocity = user:get_velocity();
		-- 	expirationtime = 0.5;
		-- 	size = 4;
		-- 	glow = 14;
		-- 	texture = u.image('sorcery_crackle.png'):multiply(u.color(sorcery.data.spells[spell].color):brighten(1.3)):render();
		-- 	animation = {
		-- 		type = 'vertical_frames';
		-- 		aspect_w = 16, aspect_h = 16;
		-- 		length = 0.6;
		-- 	}
		-- }
		local fac = matprops and matprops.sturdiness or 1
		stack:add_wear((sorcery.wands.util.wear(stack) / fac) * context.wearmult)
	end
	return stack
end


sorcery.wands.util.enumerate_kinds = function()
	local addid = function(k)
		k.id = sorcery.wands.util.baseid(k)
		return k
	end

	local kinds = {}
	for woodname, wood in pairs(sorcery.wands.materials.wood) do
		kinds[#kinds + 1] = addid {wood = woodname}
		for gemname,  gem  in pairs(sorcery.wands.materials.gem) do
			kinds[#kinds + 1] = addid { wood = woodname; gem = gemname; }
		end
		for wirename, wire in pairs(sorcery.wands.materials.wire) do
			kinds[#kinds + 1] = addid {wood = woodname, wire = wirename}
			for gemname,  gem  in pairs(sorcery.wands.materials.gem) do
				kinds[#kinds + 1] = addid {
					wood = woodname;
					wire = wirename;
					gem = gemname;
				}
			end
		end
	end 
	return kinds
end

-- TODO: allow new kinds of wood, wire, and gems to be
-- registered in the usual way

sorcery.wands.util.imgfor = function(kind)
	local img = {}
	img.base = u.image('sorcery_wand_base_' .. kind.wood .. '.png')
	img.whole = img.base
	if kind.wire then
		img.wire = u.image('sorcery_wand_' .. kind.wire .. '_wire.png')
		img.whole = img.wire:blit(img.whole)
	end
	if kind.gem then
		img.gem = u.image('sorcery_wand_' .. kind.gem .. '_tip.png')
		img.whole = img.gem:blit(img.whole)
	end
	return img
end

local update_stand_info = function(pos)
	local woodname = minetest.registered_nodes[minetest.get_node(pos).name]._proto.wood
	local meta = minetest.get_meta(pos)
	local inv = meta:get_inventory()
	if inv:is_empty('wand') then
		meta:set_string('infotext',u.str.capitalize(woodname) .. ' wand stand')
	else
		local stack = inv:get_stack('wand',1)
		local spell = stack:get_meta():get_string('sorcery_wand_spell')
		local color = u.color(127,127,127)
		if spell ~= '' then
			color = u.color(sorcery.data.spells[spell].color):readable()
		end
		local wand_proto = sorcery.wands.util.getproto(stack)
		meta:set_string('infotext',color:fmt(sorcery.wands.util.fullname(stack) .. ' stand'))
	end
end
local createstand = function(name, wood, desc, tex, extra)
	local hitbox = {
		type = "fixed";
		fixed = {
			-0.5, -0.5, -0.3;
			0.5, -0.1, 0.3;
		};
	}
	local images = {}
	for k,v in pairs(tex) do images[k] = v:render() end
	local auto = {
		description = desc;
		drawtype = 'mesh';
		mesh = 'sorcery-wand-stand.obj';
		sunlight_propagates = true;
		paramtype = 'light';
		paramtype2 = 'facedir';
		tiles = images;
		selection_box = hitbox;
		collision_box = hitbox;
		after_dig_node = sorcery.lib.node.purge_container;
		use_texture_alpha = 'blend';
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size('wand', 1)
			update_stand_info(pos)
		end;
		_proto = {
			wood = wood;
		};
		groups = {
			sorcery_wand_stand = 1;
			choppy = 2;
			dig_immediate = 2;
		};
	}
	minetest.register_node(name, u.tbl.merge(auto,extra))
end
local rack_update = function(pos)
	local meta = minetest.get_meta(pos)
	local inv = meta:get_inventory()
	local wandcount = 0
	local mkslot = function(slot,x,y)
		local stack = inv:get_stack('wands',slot)
		local im = stack:get_meta()
		local r = string.format([[ list[context;wands;%f,%f;1,1;%u] ]], x, y, slot-1)
		if stack:is_empty() then
			r = r .. string.format([[ image[%f,%f;1,1;sorcery_ui_ghost_wand.png] ]], x, y)
		else
			wandcount = wandcount + 1
			if im:contains('sorcery_wand_spell') then
				local spell = im:get_string('sorcery_wand_spell')
				local spname = u.str.capitalize(sorcery.data.spells[spell].name)
				local spclr = u.color(sorcery.data.spells[spell].color or {255,255,255})

				r = r .. string.format([[
					label[%f,%f;%s]
				]], x + 1.25, y + 0.5, minetest.formspec_escape(spclr:fmt(spname)))
			end
		end
		return r
	end
	local spec = [[
		formspec_version[3] size[10.25,11.75] real_coordinates[true]
		list[current_player;main;0.25,6.75;8,4;]
		listring[current_player;main] listring[context;wands]
	]]
	for i=1,inv:get_size('wands') do
		local yo = ((i-1) % 5) * 1.25
		local xo = i > 5 and 5 or 0
		spec = spec .. mkslot(i, 0.25 + xo, 0.25 + yo)
	end
	meta:set_string('formspec',spec)
	if wandcount > 0 then
		meta:set_string('infotext',string.format('Wand rack with %u wands', wandcount))
	else
		meta:set_string('infotext','Wand rack')
	end
end

local createrack = function(name, wood, tex, desc)
	local hitbox = {
		type = "fixed";
		fixed = {
			-0.5, -0.5, 0.5;
			 0.5,  0.5, 0.35;
		};
	}
	local rack = {
		description = desc;
		drawtype = 'mesh';
		mesh = 'sorcery-wand-rack.obj';
		sunlight_propagates = true;
		paramtype = 'light';
		paramtype2 = 'facedir';
		tiles = {
			u.image('default_diamond_block.png'):
				multiply(u.color{50,255,70}):render();
			tex:render();
			'default_diamond_block.png';
			'default_copper_block.png';
			'default_pine_wood.png';
			'default_junglewood.png';
		};
		selection_box = hitbox;
		collision_box = hitbox;
		use_texture_alpha = 'blend';
		after_dig_node = sorcery.lib.node.purge_container;
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size('wands',10)
			rack_update(pos)
		end;
		allow_metadata_inventory_put = function(pos, lst, idx, stack, user)
			if minetest.get_item_group(stack:get_name(), 'sorcery_wand') ~= 0
				then return 1
				else return 0
			end
		end;
		on_metadata_inventory_move = rack_update;
		on_metadata_inventory_take = rack_update;
		on_metadata_inventory_put = rack_update;
		_proto = { wood = wood; };
		groups = {
			sorcery_wand_rack = 1;
			choppy = 2;
			dig_immediate = 2;
		};
	}
	minetest.register_node(name, rack)
end
for woodname, wood in pairs(sorcery.wands.materials.wood) do
	local blank = u.image('sorcery_transparent.png'); -- haaaaack
	local name = 'sorcery:wand_stand_' .. woodname
	local rackid = 'sorcery:wand_rack_' .. woodname
	createstand(name, woodname,
		u.str.capitalize(woodname) .. 'wood wand stand',
		{ wood.tex; blank; blank; blank; }, {
			on_rightclick = function(pos,node,user,stack)
				local meta = minetest.get_meta(pos)
				local stand = meta:get_inventory()
				local wand_proto = sorcery.wands.util.getproto(stack)
				if minetest.get_item_group(stack:get_name(), 'sorcery_wand') ~= 0 then
					stand:set_stack('wand',1,stack)
					minetest.swap_node(pos, {
						name = sorcery.wands.util.baseid(wand_proto) .. '_stand_' .. woodname;
						param2 = node.param2;
					})
					stack = ItemStack(nil)
				end
				update_stand_info(pos)
				return stack
			end
		}
	)
	createrack(rackid,woodname,wood.tex,
		u.str.capitalize(woodname) .. 'wood wand rack')

	local plank = wood.plank or 'default:' .. woodname .. '_wood'
	minetest.register_craft {
		output = name;
		recipe = {
			-- TODO: whittling/carving knife
			{plank, 'default:stick', plank};
			{plank, plank, plank};
		};
	}

	minetest.register_craft {
		output = rackid;
		recipe = {
			{plank,'default:stick',plank};
			{'sorcery:screw_steel','screwdriver:screwdriver','sorcery:screw_steel'};
			{plank,name,plank};
		};
		replacements = {
			{'screwdriver:screwdriver','screwdriver:screwdriver'};
		};
	}
end

sorcery.wands.createstands = function(kind)
	-- big oof
	for woodname, wood in pairs(sorcery.wands.materials.wood) do
		local gemimg = u.image('default_diamond_block.png')
		if kind.gem
			then gemimg = gemimg:multiply(u.color(sorcery.wands.materials.gem[kind.gem].tone))
			else gemimg = gemimg:fade(1) end

		createstand(
			kind.id .. '_stand_' .. woodname, 
			woodname,
			u.str.capitalize(woodname) .. 'wood wand stand with ' ..  string.lower(sorcery.wands.util.basename(kind,true)), {
				wood.tex;
				sorcery.wands.materials.wood[kind.wood].tex;
				gemimg;
				(kind.wire and sorcery.wands.materials.wire[kind.wire].tex) or u.image('sorcery_transparent.png'); -- haaaaack
			}, {
				drop = 'sorcery:wand_stand_' .. woodname;
				after_dig_node = function(pos,node,meta,digger)
					local stack = meta.inventory.wand[1]
					if stack and not stack:is_empty() then
						-- luv 2 defensive coding
						minetest.add_item(pos, stack)
					end
				end;
				groups = {
					not_in_creative_inventory = 1;
					sorcery_wand_stand = 1;
					choppy = 2;
					dig_immediate = 2;
				};
				on_rightclick = function(pos,node,user,stack)
					local meta = minetest.get_meta(pos)
					local stand = meta:get_inventory()
					local wand = stand:get_stack('wand',1)
					if stack:is_empty() then
						stack = wand
					else
						local inv = user:get_inventory()
						if inv:room_for_item('main', wand) then
							inv:add_item('main', wand)
						else goto failure end
					end
					stand:set_stack('wand',1,ItemStack(nil))
					minetest.swap_node(pos, {
						name = 'sorcery:wand_stand_' .. woodname;
						param2 = node.param2;
					})
					::failure::
						update_stand_info(pos)
						return stack
				end;
			}
		)
	end
end
sorcery.wands.createkind = function(kind)
	if sorcery.wands.kinds[kind.id] then return false end

	local basis = u.tbl.copy(kind)
	basis.img = sorcery.wands.util.imgfor(basis)

	minetest.register_tool(kind.id, {
		description = u.ui.tooltip {
			title = sorcery.wands.util.basename(kind);
			desc = sorcery.wands.util.basedesc(kind);
		};
		inventory_image = basis.img.whole:render();
		groups = {
			tool = 1;
			sorcery_wand = 1;
			not_in_creative_inventory = 1;
		};
		node_dig_prediction = "";
		on_use = wand_cast;
		on_secondary_use = function(stack,user,target)
			return wand_cast(stack,user,nil)
		end;
		-- on_place if we need to use rightclick
		_proto = basis;
	})
	sorcery.wands.createstands(kind)

	return true
end

sorcery.wands.createallkinds = function()
	local allkinds = sorcery.wands.util.enumerate_kinds()
	for _,k in pairs(allkinds) do sorcery.wands.createkind(k) end
end

local update_wand_description = function(stack)
	local proto = sorcery.wands.util.getproto(stack)
	local wm = stack:get_meta()
	local spell = wm:get_string('sorcery_wand_spell')
	if spell ~= "" then
		local sd = sorcery.data.spells[spell]
		wm:set_string('description', u.ui.tooltip {
			title = sorcery.wands.util.fullname(stack);
			desc = sorcery.wands.util.basedesc(proto);
			props = {
				{ color = u.color(sd.color); desc = sd.desc }
			};
		})
	else
		wm:set_string('description', u.ui.tooltip {
			title = sorcery.wands.util.basename(proto);
			desc = sorcery.wands.util.basedesc(proto);
		})
	end
	return stack
end

sorcery.wands.createallkinds()

local wandwork_soak = function(pos, elapsed)
	-- first, make sure there's still water in the station
	local node = minetest.get_node(pos)
	if node.name ~= 'sorcery:wandworking_station_water' then return false end

	local meta = minetest.get_meta(pos)
	local inv = meta:get_inventory()
	local philters, wands = {}, {}
	for i=0,inv:get_size('tank') do
		local stack = inv:get_stack('tank', i)
		if minetest.get_item_group(stack:get_name(),'sorcery_philter') > 0 then
			philters[#philters + 1] = { idx = i, stack = stack }
		elseif minetest.get_item_group(stack:get_name(),'sorcery_wand') > 0 then
			wands[#wands + 1] = { idx = i, stack = stack }
		end
	end

	if #wands == 0 or #philters == 0 then return false end
	local duration = 600
	--((4 * #wands) + (2 * #philters)) * 1 -- 600
	--nonconstant duration doesn't actually make sense, unless we
	--base it on the specific wood and philters
	local totaltime = elapsed + meta:get_float('soaktime')
	if totaltime < duration then
		meta:set_float('soaktime',totaltime)
		return true
	end
	meta:set_float('soaktime',0)

	-- iterate through every wand in the tank to see if anything
	-- needs to be done
	local success = false
	for i,wand in pairs(wands) do
		local proto = wand.stack:get_definition()._proto
		local wood = sorcery.wands.materials.wood[proto.wood]
		for spell,def in pairs(sorcery.data.spells) do
			if not def.affinity then goto skip_spell end
			local potions, found = {}, false
			for _,a in pairs(def.affinity) do
				if a == proto.wood then found = true else
					potions[#potions + 1] = a
				end
			end
			if not found then goto skip_spell end
			-- check whether all the necessary philters
			-- are accounted for
			if #potions ~= #philters then goto skip_spell end
			for _,p in pairs(potions) do
				for _,ph in pairs(philters) do
					local def = ph.stack:get_definition()
					if def._protoname == p then goto found end
				end
				goto skip_spell
			::found::end

			success = true
			-- we've confirmed that all the philters for this spell
			-- are present, now we need to apply it to the wand
			wand.stack:get_meta():set_string('sorcery_wand_spell',spell)
			update_wand_description(wand.stack)
			inv:set_stack('tank',wand.idx,wand.stack)

			do break end -- }:<<<
		::skip_spell::end
	end

	if success == true then
		-- empty out the philters
		for _,v in pairs(philters) do
			inv:set_stack('tank', v.idx, ItemStack('vessels:glass_bottle'))
		end

		-- drain out the water
		minetest.swap_node(pos, {
			name = 'sorcery:wandworking_station';
			param2 = node.param2;
		})
	end
	return false
end;

local wandwork_form = function(pos, water)
	local slot = function(name,x,y,w,h)
		return string.format('list[nodemeta:%d,%d,%d;%s;%f,%f;%f,%f;]',
			pos.x, pos.y, pos.z,
			name, x,y, w,h
		)
	end

	return 'size[8,6.25]' ..
		slot('tank', 0.5,0, 2,2) ..
		slot('wandparts', 5.5,0, 1,2) ..
		slot('preview', 6.5,0.5, 1,1) ..
		slot('input', 4.5,0.5, 1,1) .. [[
		list[current_player;main;0,2.5;8,4;]
		listring[]
	]]
end

local wandwork_rightclick = function(pos,node,user,stack)
	local swapout = function(new)
		minetest.swap_node(pos, {
			name = new;
			param2 = node.param2;
		})
	end
	local water = node.name == 'sorcery:wandworking_station_water'
	if water then
		if stack:get_name() == 'bucket:bucket_empty' then
			swapout('sorcery:wandworking_station')
			stack = ItemStack('bucket:bucket_water')
		else goto nobucket end
	else
		if minetest.get_item_group(stack:get_name(), 'water_bucket') > 0 then
			swapout('sorcery:wandworking_station_water')
			stack = ItemStack('bucket:bucket_empty')
		else goto nobucket end
	end
	do return stack end

	::nobucket::
		minetest.show_formspec(user:get_player_name(),'sorcery:wandstation_inventory',wandwork_form(pos,water))
end

local wand_in_station = function(inv)
	local wand = inv:get_stack('input',1)
	if wand:is_empty() or minetest.get_item_group(wand:get_name(), 'sorcery_wand') == 0
		then return false
		else return true
	end
end

local ensure_wand = function(inv)
	if not wand_in_station(inv) then
		for i=1,inv:get_size('wandparts') do
			inv:set_stack('wandparts',i,ItemStack(nil))
		end
		return false
	end
	return true
end

local update_wand = function(inv)
	if not ensure_wand(inv) then return false end
	local parts = inv:get_list('wandparts')
	local oldwand = inv:get_stack('input',1)
	local newwand = {
		wood = oldwand:get_definition()._proto.wood;
	}
	for i=1,#parts do
		if parts[i]:is_empty() then goto skip end
		local item = parts[i]:get_name()
		for name,gem in pairs(sorcery.wands.materials.gem) do
			local id = (gem.foreign or 'sorcery:gem_' .. name)
			if item == id then newwand.gem = name break end
		end
		for name,wire in pairs(sorcery.wands.materials.wire) do
			local id = (wire.foreign or 'basic_materials:' .. name .. '_wire')
			if item == id then newwand.wire = name break end
		end
	::skip::end
	--print(newwand.wood,newwand.gem,newwand.wire)
	oldwand:set_name(sorcery.wands.util.baseid(newwand))
	update_wand_description(oldwand)
	inv:set_stack('input',1, oldwand)
end

local disasm_wand = function(stack,wwi)
	if not ensure_wand(wwi) then return false end
	if minetest.get_item_group(stack:get_name(), 'sorcery_wand') ~= 0 then
		local wand = stack:get_definition()._proto
		if wand.gem then
			local gem = sorcery.wands.materials.gem[wand.gem]
			local id = gem.foreign or 'sorcery:gem_' .. wand.gem
			wwi:set_stack('wandparts',1,ItemStack(id))
		end
		if wand.wire then
			local wire = sorcery.wands.materials.wire[wand.wire]
			local id = wire.foreign or 'basic_materials:' .. wand.wire .. '_wire'
			wwi:set_stack('wandparts',2,ItemStack(id))
		end
		return true
	else return false end
end

local find_core = function(stack)
	for k,v in pairs(sorcery.wands.materials.core) do
		if stack:get_name() == v.item then return k,v end
	end
	return nil
end

local update_preview = function(wwi)
	if wwi:is_empty('preview') then return end
	local corename, core
	for i=1,wwi:get_size('wandparts') do
		corename, core = find_core(wwi:get_stack('wandparts',i))
		if corename ~= nil then break end
	end
	
	local wand = wwi:get_stack('preview',1)
	local meta = wand:get_meta()
	meta:set_string('sorcery_wand_core',corename)
	wwi:set_stack('preview',1,update_wand_description(wand))
end

local station_handle_input = function(stack,wwi)
	for name,wood in pairs(sorcery.wands.materials.wood) do
		if stack:get_name() == (wood.tree or 'default:' .. name .. '_tree') then
			wwi:set_stack('preview',1,ItemStack{
				name = 'sorcery:wand_' .. name; wear = 0;
			})
			goto found
		end
	end
	wwi:set_stack('preview',1,ItemStack(nil))

	if disasm_wand(stack,wwi) then return end
	::found:: update_preview(wwi)
end

local replace_input = function(wwi)
	local treename = wwi:get_stack('input',1):get_name()
	wwi:set_stack('input',1, ItemStack {
		name = treetoplank[treename];
		count = 3;
	})
	for i=1,wwi:get_size('wandparts') do
		wwi:set_stack('wandparts',i,ItemStack(nil))
	end
end

local register_station = function(water)
	local tilebase = {
		"default_aspen_wood.png";
		"sorcery_wandworking_station_side.png";
	}
	local base = {
		description = "Wandworking Station";
		groups = {
			choppy = 2;
			not_in_creative_inventory = water and 1 or nil;
		};
		paramtype2 = 'facedir';
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size('tank',4)
			inv:set_size('input',1)
			inv:set_size('wandparts',2)
			inv:set_size('preview',1)
			-- meta:set_string('formspec',wandwork_form(false))
		end;
		on_timer = function(...)
			if water then return wandwork_soak(...);
			         else return false end
		end;
		on_rightclick = wandwork_rightclick;
		after_dig_node = sorcery.lib.node.purge_container;
		allow_metadata_inventory_put = function(pos, list, index, stack, user)
			local meta = minetest.get_meta(pos)
			local wwi = meta:get_inventory()
			if list == 'preview' then return 0
			elseif list == 'wandparts' then
				if (not wwi:is_empty('preview')) and
					find_core(stack) then return 1
				elseif not wand_in_station(wwi) then return 0 end
			elseif list == 'tank' then
				if water then return 1 else return 0 end
			end
			return 1
		end;
		allow_metadata_inventory_move = function(pos, fromlist, fromidx, tolist, toidx, count, user)
			if tolist == 'preview' then return 0
			elseif tolist == 'wandparts' then
				local meta = minetest.get_meta(pos)
				local wwi = meta:get_inventory()
				if (not wwi:is_empty('preview')) and
					find_core(wwi:get_stack(fromlist,fromidx)) then return 1
				elseif not
					wand_in_station(minetest.get_meta(pos):get_inventory())
				then return 0 end
			elseif tolist == 'tank' then
				if water then return 1 else return 0 end
			end
			return 1
		end;
		on_metadata_inventory_move = function(pos, fromlist, fromidx, tolist, toidx, count, user) 
			local meta = minetest.get_meta(pos)
			local wwi = meta:get_inventory()
			if fromlist == 'preview' then
				replace_input(wwi)
			elseif fromlist == 'input' or tolist == 'input' then
				if wwi:is_empty('preview') then
					for i=1,wwi:get_size('wandparts') do
						wwi:set_stack('wandparts',i,ItemStack(nil))
					end
				else wwi:set_stack('preview',1,ItemStack(nil)) end

				station_handle_input(wwi:get_stack(tolist,toidx),wwi)
			elseif fromlist == 'wandparts' or tolist == 'wandparts' then
				if wwi:is_empty('preview') then update_wand(wwi)
				else update_preview(wwi) end
			elseif tolist == 'tank' then
				minetest.get_node_timer(pos):start(1)
			end
		end;
		on_metadata_inventory_take = function(pos, list, index, stack, user)
			local meta = minetest.get_meta(pos)
			local wwi = meta:get_inventory()
			if list == 'preview' then
				replace_input(wwi)
			elseif list == 'input' then
				if wwi:is_empty('preview') then
					for i=1,wwi:get_size('wandparts') do
						wwi:set_stack('wandparts',i,ItemStack(nil))
					end
				else wwi:set_stack('preview',1,ItemStack(nil)) end
			elseif list == 'wandparts' then
				if wwi:is_empty('preview') then update_wand(wwi)
				else update_preview(wwi) end
			end
		end;
		on_metadata_inventory_put = function(pos, list, index, stack, user)
			local meta = minetest.get_meta(pos)
			local wwi = meta:get_inventory()
			if list == 'input' then
				station_handle_input(stack,wwi)
			elseif list == 'wandparts' then
				if wwi:is_empty('preview') then update_wand(wwi)
				else update_preview(wwi) end
			elseif list == 'tank' then
				minetest.get_node_timer(pos):start(1)
			end
		end;
		_sorcery = {
			recipe = {
				note = 'Construct wands from tree blocks and gems; fill with water and soak wands with philters to enchant them';
			};
		};
	}

	local id
	if water then
		id = 'sorcery:wandworking_station_water'
		base = u.tbl.merge(base, {
			description = "Wandworking Station (full)";
			tiles = u.tbl.append({"sorcery_wandworking_station_top_water.png"}, tilebase)
		})
	else
		id = 'sorcery:wandworking_station'
		base = u.tbl.merge(base, {
			description = "Wandworking Station";
			drop = "sorcery:wandworking_station";
			tiles = u.tbl.append({"sorcery_wandworking_station_top.png"}, tilebase)
		})
	end
	minetest.register_node(id, base)
end

register_station(false)
register_station(true)

minetest.register_craft {
	recipe = {
		{ 'stairs:slab_stone', 'default:steel_ingot', 'stairs:slab_stone' };
		{ 'group:wood', 'bucket:bucket_empty', 'group:wood' };
		{ 'group:wood', 'group:wood', 'group:wood' };
	};
	output = 'sorcery:wandworking_station';
}