Initial commit

main
copygirl 7 months ago
commit 0757a58d16
  1. 9
      .editorconfig
  2. 106
      CONCEPT.md
  3. 20
      README.md
  4. 336
      contraption.lua
  5. 68
      init.lua
  6. 3
      mod.conf
  7. 103
      search.lua

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

@ -0,0 +1,106 @@
# Contraptions Concept
## Progression
- Assemble wooden contraption inside a rectangle of frames
- Rotate a contraption
- Take a contraption to water
- Use a shovel to propel a contraption on water
- Craft a wooden wheel
- Attach wheels to a contraption
- Construct a wooden rail (gravel + staff?)
- Push a contraption or contraption on a rail
- Attach a handle to a contraption
- Pull a contraption using a handle
- Assemble a hinged contraption
- Rotate a hinged contraption using a handle
- Assemble a contraption inside a cuboid of lode frames
- Craft a lode wheel
- Construct a lode rail (gravel + lode rod?)
- Build a self-propelled contraption
- Ride a contraption going full speed
## Materials
### Contraption Tiers
- Wooden: Plank, Log
- ???
### Ground
Different types of ground apply different amount of friction onto a contraption, slowing it down. These are just some rough guessed values to play around with.
- dirty: 2
- plank: 1
- cobble: 1
- smooth: 0.5
- Tarstone: 0.3
- Water: 0.4
- Wooden Rail: 0.5
- Lode Rail: 0.2
## Details
### Assemble wooden contraption inside a rectangle of frames
```
++++
+PP+ + = Wooden Frame
+PP+ P = Plank
+PP+ (Pummel frame with hammer to complete.)
++++
```
A contraption is a connected set of nodes that can be pushed by players to move it. Multiple players actively pushing reduces the cooldown between each push. Larger contraptions are more difficult to push.
contraptions are affected by gravity.
contraptions will drag with them any "pushable" nodes (such as items) directly above them. If a contraption is pushed underneath an overhang and items can't follow, they will fall off.
### Turn a contraption
???
Plaforms can turn gradually, their "forward" direction changing to an angle not perfectly aligned with the grid.
```
LLLL LLL
RRRR RRRL LL
R RRLL
RR
L
RL
RL
RL
R
RL
RL
RL
RL
RL
RL
RL
RL
RL
RL
RL
RL
```
### Craft a wheel
???
### Attach wheels to a contraption to create a cart
```
WPPW P = Wooden contraption
PP W = Wooden Wheel
WPPW (Pummel wheel into contraption to complete.)
```
A wheeled contraption is able to build up momentum, loosing it due to passive speed loss and friction. Different surfaces have different amounts of friction.

@ -0,0 +1,20 @@
# NodeCore Contraptions Mod
This mod for [NodeCore], a game written for the open source [Minetest] engine,
adds so-called "contraptions" to the game, which are node-based objects that
can be moved around the world as a connected structure.
Contraptions can be platforms, rafts, carts, boats, doors, minecarts, vehicles
and even trains. Or, at least, that's the plan. This mod is still heavily in
development. Items and players riding on top or inside contraption will be
pushed along with them.
These contraptions are inspired by the *Minecraft* mod [Create], which allows
creation of similar contraptions, however this implementation will achieve
this effect by using purely nodes in their usual grid. Rotation will be done
in a similar fashion as the vehicles in [Cataclysm: Dark Days Ahead].
[NodeCore]: https://content.minetest.net/packages/Warr1024/nodecore/
[Minetest]: https://minetest.net/
[Create]: https://modrinth.com/mod/create
[Cataclysm: Dark Days Ahead]: https://cataclysmdda.org/

@ -0,0 +1,336 @@
local math, pairs, ipairs, setmetatable, table_insert, print
= math, pairs, ipairs, setmetatable, table.insert, print
local minetest, vector, nodecore
= minetest, vector, nodecore
local mod_name = minetest.get_current_modname()
local mod_storage = minetest.get_mod_storage()
-- This is the object that is returned by this script.
local contraption = {}
-- Some constants. Should probably be removed.
local AIR = { name = "air" }
local PLANK = { name = "nc_woodwork:plank" }
local CONTRAPTION_WOOD = { name = "nc_contraptions:contraption_wood" }
-- Counts up to infinity as contraptions get created.
-- Each contraption gets assigned a unique id for its lifetime.
local id_counter = 0
-- Table containing the currently active contraptions keyed by their id.
local active_contraptions = {}
-- TODO: Push along items.
-- TODO: Push along players.
-- TODO: Friction depending on ground.
-- TODO: Gravity.
-- TODO: Water physics!
-- TODO: Rotation.
-- TODO: Unload and save contraptions when they're in unloaded chunks.
-- TODO: Use meta field `infotext` for displaying debug info about a contraption?
local function error(...)
local str = "[" .. mod_name .. "] "
for _, v in ipairs({...}) do str = str + tostring(v) end
print(str)
end
--------------------
-- EVENT HANDLING --
--------------------
function contraption.on_global_step(delta_time)
for _, contrap in pairs(active_contraptions) do
contrap:update(delta_time)
end
end
function contraption.on_startup()
local version = mod_storage:get_int("version")
if version == 0 then
-- Assume that no mod data has been saved yet.
return
elseif version ~= 1 then
error("Unknown mod storage version " .. version)
return
end
id_counter = mod_storage:get_int("id_counter")
local contraptions_str = mod_storage:get("contraptions")
local contraptions = minetest.deserialize(contraptions_str)
for _, id in ipairs(contraptions) do
local str = mod_storage:get("contraption_" .. id)
contraption.load_from_string(id, str)
end
end
function contraption.on_shutdown()
-- TODO: Only modify mod storage where contraptions have changed / been destroyed.
-- For now we're just going to wipe the storage and re-create it.
mod_storage:from_table(nil)
mod_storage:set_int("version", 1)
mod_storage:set_int("id_counter", id_counter)
local contraptions = {}
for id, contrap in pairs(active_contraptions) do
local str = contrap:save_to_string()
mod_storage:set_string("contraption_" .. id, str)
table_insert(contraptions, id)
end
mod_storage:set_string("contraptions", minetest.serialize(contraptions));
end
----------------------
-- PUBLIC FUNCTIONS --
----------------------
function contraption.find(pos)
-- First check if the node at `pos` is a contraption block.
for _, c in pairs(active_contraptions) do
if pos.x >= c.region.min.x and pos.x <= c.region.max.x
and pos.y >= c.region.min.y and pos.y <= c.region.max.y
and pos.z >= c.region.min.z and pos.z <= c.region.max.z
then
local rel_pos = pos - c.region.min
local rel_pos_hash = minetest.hash_node_position(rel_pos)
if c.nodes[rel_pos_hash] then return c end
end
end
return nil
end
-------------------------------------
-- CONTRAPTION TYPE IMPLEMENTATION --
-------------------------------------
local metatable = { __index = contraption }
contraption.metatable = metatable
function contraption.create(region)
local nodes = {}
local num_nodes = 0
for x = region.min.x, region.max.x do
for y = region.min.y, region.max.y do
for z = region.min.z, region.max.z do
local pos = vector.new(x, y, z)
local node = minetest.get_node(pos)
if nodecore.match(node, PLANK) then
local offset = pos - region.min
local pos_hash = minetest.hash_node_position(offset)
nodes[pos_hash] = CONTRAPTION_WOOD
num_nodes = num_nodes + 1
end
end
end
end
-- Less than 4 nodes don't make a contraption.
if num_nodes < 4 then return nil end
-- TODO: Shrink region to the minimum required cuboid.
-- In the above code we could just use pos as table key.
-- TODO: Flood fill algorithm to make sure all nodes are connected.
id_counter = id_counter + 1
local result = setmetatable({
id = id_counter,
region = region,
nodes = nodes,
num_nodes = num_nodes,
}, metatable)
-- Change nodes from base nodes to their contraption version.
for pos_hash, node in pairs(nodes) do
local rel_pos = minetest.get_position_from_hash(pos_hash)
local abs_pos = vector.add(region.min, rel_pos)
minetest.set_node(abs_pos, node)
end
active_contraptions[result.id] = result
return result
end
function contraption.load_from_string(id, str)
local obj = minetest.deserialize(str)
if obj.version ~= 1 then
error("Unknown contraption version " .. obj.version)
return nil
end
local result = setmetatable({
id = id,
region = obj.region,
nodes = obj.nodes,
num_nodes = obj.num_nodes,
motion = obj.motion and vector.copy(obj.motion ) or nil,
partial = obj.partial and vector.copy(obj.partial) or nil,
}, metatable)
active_contraptions[id] = result
return obj
end
function contraption:save_to_string()
local to_save = {
version = 1,
region = self.region,
nodes = self.nodes,
num_nodes = self.num_nodes,
motion = self.motion,
partial = self.partial,
}
return minetest.serialize(to_save)
end
function contraption:destroy()
active_contraptions[self.id] = nil
for pos_hash, node in pairs(self.nodes) do
local rel_pos = minetest.get_position_from_hash(pos_hash)
local abs_pos = vector.add(self.region.min, rel_pos)
local def = minetest.registered_nodes[node.name]
minetest.set_node(abs_pos, def.drop_in_place)
end
end
function contraption:push(pusher, dir)
-- See if the same player has already pushed this contraption recently.
-- If so, do not apply the full amount of motion from the push.
local pusher_name = pusher.get_player_name and pusher:get_player_name() or ""
self.recent_pusher = self.recent_pusher or {}
local delay = self.recent_pusher[pusher_name]
if delay then dir = dir * (1 - math.min(1, delay)) end
self.recent_pusher[pusher_name] = 1.0 -- seconds
self.motion = self.motion or vector.zero()
self.motion = vector.add(self.motion, dir)
end
function contraption:update(delta_time)
if not self.motion then return end
-- Partial motion that accumulates over time,
-- since we can only move in increments of 1.
self.partial = self.partial or vector.zero()
self.partial = self.partial + self.motion * delta_time
-- If partial motion has increased to more than 1 on
-- one axis, find out which axis has the largest motion ..
local max = 0
local which = 0
for i = 1, 3 do
local abs = math.abs(self.partial[i])
if abs >= 1 and abs > max then
max = abs
which = i
end
end
-- .. and move it 1 step into that direction.
if which ~= 0 then
local step = vector.zero()
step[which] = math.sign(self.partial[which])
if self:move(step) then
self.partial = self.partial - step
else
-- Reset motion into the direction that we bumped into.
-- Allows for "sliding" along walls instead of stopping outright.
self.motion[which] = 0
self.partial[which] = 0
end
end
-- Apply friction.
local FRICTION = 0.25
self.motion = self.motion * (1 - FRICTION * delta_time)
-- If motion is small enough, unset it for performance.
if self.motion:length() < 0.1 then
self.motion = nil
self.partial = nil
end
if self.recent_pusher then
local has_pushers = false
for pusher, delay in pairs(self.recent_pusher) do
if delay > 0 then
self.recent_pusher[pusher] = math.max(0, delay - delta_time)
has_pushers = true
end
end
-- If there are no active pushers, delete the `recent_pushers` table.
if not has_pushers then self.recent_pusher = nil end
end
end
function contraption:move(offset)
local to_set = {} -- Nodes that need to be modified.
local to_clear = {} -- Nodes to be cleared (to AIR).
-- Assume that every node this contraption occupies needs to be cleared.
-- We'll remove entries from `to_clear` when we know a node is moved there.
for pos_hash in pairs(self.nodes) do to_clear[pos_hash] = AIR end
for old_rel_pos_hash, node in pairs(self.nodes) do
local old_rel_pos = minetest.get_position_from_hash(old_rel_pos_hash)
local old_abs_pos = vector.add(self.region.min, old_rel_pos)
local new_abs_pos = vector.add(old_abs_pos, offset)
local new_rel_pos = vector.add(old_rel_pos, offset)
local new_rel_pos_hash = minetest.hash_node_position(new_rel_pos)
-- Node is being moved to this position, no need to clear here.
to_clear[new_rel_pos_hash] = nil
local self_overlap = self.nodes[new_rel_pos_hash]
if self_overlap then
if not nodecore.match(node, self_overlap) then
to_set[new_rel_pos_hash] = node
end
else
local new_node = minetest.get_node(new_abs_pos)
if nodecore.buildable_to(new_node) then
to_set[new_rel_pos_hash] = node
else
-- We bumped into a wall or something.
return false
end
end
end
-- Set nodes that need to be changed.
for pos_hash, node in pairs(to_set) do
local rel_pos = minetest.get_position_from_hash(pos_hash)
local abs_pos = vector.add(self.region.min, rel_pos)
minetest.set_node(abs_pos, node)
end
-- Clear nodes that need to be cleared (to AIR).
for pos_hash, node in pairs(to_clear) do
local rel_pos = minetest.get_position_from_hash(pos_hash)
local abs_pos = vector.add(self.region.min, rel_pos)
minetest.set_node(abs_pos, node)
end
-- Move the contraption in the world.
self.region.min = self.region.min + offset
self.region.max = self.region.max + offset
return true
end
return contraption

@ -0,0 +1,68 @@
local minetest, vector, nodecore, include
= minetest, vector, nodecore, include
local search = include("search")
local contraption = include("contraption")
local mod_name = minetest.get_current_modname()
local i_stick = "nc_tree:stick"
local i_staff = "nc_tree:staff"
local i_frame = "nc_woodwork:frame"
local i_form = "nc_woodwork:form"
local i_plank = "nc_woodwork:plank"
local t_plank = "nc_woodwork_plank.png"
local t_frame_log = "(nc_tree_tree_side.png^[mask:nc_api_storebox_frame.png^[opacity:127)"
-- Load and save contraptions.
contraption.on_startup()
minetest.register_on_shutdown(contraption.on_shutdown)
-- `on_global_step` updates and moves around active contraptions.
minetest.register_globalstep(contraption.on_global_step)
-- Punching a contraption applies some force to it.
-- Multiple players can push a contraption more effectively.
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
local contrap = contraption.find(pos)
if not contrap then return end
if (not pointed_thing.above) or (not pointed_thing.under) then return end
local dir = vector.subtract(pointed_thing.under, pointed_thing.above)
contrap:push(puncher, dir)
end)
-- TODO: Provide a method to register additional types of contraption blocks.
minetest.register_node(mod_name .. ":contraption_wood", {
description = "Wooden Contraption",
tiles = { t_plank .. "^" .. t_frame_log },
sounds = nodecore.sounds("nc_tree_woody"),
groups = {
choppy = 1,
flammable = 2,
fire_fuel = 5,
},
on_dig = function(pos, node, digger)
local contrap = contraption.find(pos)
if contrap then contrap:destroy()
else minetest.node_dig(pos, node, digger) end
end,
drop_in_place = "nc_woodwork:plank",
})
-- TODO: Take priority over or hook into frame-to-form recipe to do our thing instead.
nodecore.register_craft({
label = "assemble wooden contraption",
action = "pummel",
toolgroups = { scratchy = 3 },
indexkeys = { i_frame },
nodes = { { match = i_frame } },
check = function(pos, data)
data.found_square = search.find_rectangle(pos, i_frame)
return not not data.found_square
end,
after = function(pos, data)
contraption.create(data.found_square)
end,
})

@ -0,0 +1,3 @@
name = nc_contraptions
description = Adds movable contraptions to NodeCore.
depends = nc_api_all

@ -0,0 +1,103 @@
local util = {}
local AXES = {
x = vector.new(1, 0, 0),
y = vector.new(0, 1, 0),
z = vector.new(0, 0, 1),
}
util.AXES = AXES;
-- Gets an array of vectors relative to `pos` of neighbors that have the specified `match`.
local function find_neighbors_with_name(pos, match)
local result = {}
for _, v in pairs(AXES) do
if minetest.get_node(pos - v).name == match then
table.insert(result, -v)
end
end
for _, v in pairs(AXES) do
if minetest.get_node(pos + v).name == match then
table.insert(result, v)
end
end
return result
end
util.find_neighbors_with_name = find_neighbors_with_name;
-- Moves `pos` towards `dir` while node is still `match`.
-- returns (vector, array) or nil
local function move_until_corner(pos, match, dir)
repeat pos = pos + dir
until minetest.get_node(pos + dir).name ~= match
local neighbors = find_neighbors_with_name(pos, match)
if #neighbors == 2
then return pos, neighbors
else return nil
end
end
-- Moves `pos` towards `dir`, checking that each node has exactly two neighbors
-- that of type `match`, until a corner turning towards `expected_corner_dir` is hit.
-- returns vector or nil
local function move_until_corner_with_checks(pos, match, dir, expected_corner_dir)
while true do
pos = pos + dir
local neighbors = find_neighbors_with_name(pos, match)
if #neighbors ~= 2 then return nil end
-- Get the "next direction" the piece is going.
local next_dir = neighbors[1]
if next_dir == -dir then next_dir = neighbors[2] end
if next_dir ~= dir then
if next_dir == expected_corner_dir
then return pos
else return nil
end
end
end
end
-- Finds a rectangle structure made from the specified node `match`.
-- returns { min = vector, max = vector } or nil
local function find_rectangle(pos, match)
local neighbors = find_neighbors_with_name(pos, match)
if #neighbors ~= 2 then return nil end
-- If we found a line piece (neighbors are opposite) then move in the
-- direction of the smallest xyz coordinate.
if neighbors[1] == -neighbors[2] then
-- NOTE: Due to how `get_neighbors_with_name` is written, the first element
-- is guaranteed to contain the vector pointing towards negative.
pos, neighbors = move_until_corner(pos, match, neighbors[1])
if not pos then return nil end
end
-- We're now guaranteed to be at a corner piece.
-- Move to the corner piece with the smallest xyz coordinate.
while neighbors[1].x + neighbors[1].y + neighbors[1].z < 1 do
pos, neighbors = move_until_corner(pos, match, neighbors[1])
if not pos then return nil end
end
-- Find the neighboring corners of the starting corner.
local first_corner = move_until_corner_with_checks(pos, match, neighbors[1], neighbors[2])
if not first_corner then return nil end
local second_corner = move_until_corner_with_checks(pos, match, neighbors[2], neighbors[1])
if not second_corner then return nil end
-- Continue towards the final corner from the new corners.
first_corner = move_until_corner_with_checks(first_corner, match, neighbors[2], -neighbors[1])
if not first_corner then return nil end
second_corner = move_until_corner_with_checks(second_corner, match, neighbors[1], -neighbors[2])
if not second_corner then return nil end
-- Ensure that they have met up at the same location.
if first_corner ~= second_corner then return nil end
return { min = pos, max = second_corner }
end
util.find_rectangle = find_rectangle;
return util
Loading…
Cancel
Save