322 lines
15 KiB
Lua
322 lines
15 KiB
Lua
--[[
|
|
ATM Node
|
|
--------
|
|
This file defines the Automatic Teller Machine (ATM) node and all
|
|
of its associated forms and logic for player interaction.
|
|
--]]
|
|
|
|
-- A global variable to store the position of the last used machine.
|
|
-- This is necessary for stability in the user's specific environment.
|
|
pos_info = {}
|
|
|
|
-- The interest rate for the monthly credit payment.
|
|
local credit_rate = 0.04
|
|
|
|
-- Shows the account statement form for the player using the machine.
|
|
-- @param player: The player object.
|
|
-- @param account_type: "balance" or "credit" to determine which history to show.
|
|
local function show_atm_statement_form(player, account_type)
|
|
local player_name = player:get_player_name()
|
|
local data = bank_accounts.get_account_data(player_name)
|
|
local history = data.history
|
|
local lines = {}
|
|
local current_total_label = ""
|
|
|
|
-- Header for the textlist
|
|
table.insert(lines, minetest.formspec_escape(
|
|
string.format("%-11s | %-13s | %-13s | %-20s | %-20s | %s",
|
|
S("Date"), S("Amount"), S("New Balance"), S("Method"), S("Purpose"), S("Partner"))
|
|
))
|
|
table.insert(lines, minetest.formspec_escape("-----------------------------------------------------------------------------------------"))
|
|
|
|
-- Populate the list with transaction data
|
|
if history then
|
|
for _, t in ipairs(history) do
|
|
if t.account == account_type then
|
|
local parts = {}
|
|
table.insert(parts, string.format("%-11s", os.date("%Y-%m-%d", t.timestamp)))
|
|
table.insert(parts, string.format("%12s", string.format("%+.2f", t.amount) .. " MG"))
|
|
table.insert(parts, string.format("%12s", string.format("%.2f", t.new_total).." MG"))
|
|
table.insert(parts, string.format("%-20s", S(t.type)))
|
|
|
|
local purpose = t.purpose or ""
|
|
local partner = t.other or ""
|
|
if purpose ~= "" or partner ~= "" then
|
|
table.insert(parts, string.format("%-20s", purpose))
|
|
if partner ~= "" then
|
|
table.insert(parts, partner)
|
|
end
|
|
end
|
|
table.insert(lines, minetest.formspec_escape(table.concat(parts, " | ")))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Set the label for the current total based on the selected account type
|
|
if account_type == "balance" then
|
|
current_total_label = S("Current Balance: @1", string.format("%.2f", data.balance) .. " MG")
|
|
else
|
|
current_total_label = S("Current Credit Debt: @1", string.format("%.2f", data.credit) .. " MG")
|
|
end
|
|
|
|
local form_name = "bank_accounts:atm_statement@" .. player_name
|
|
local formspec = "size[13,9]" ..
|
|
"label[0,0;"..S("Account Statement for @1", player_name).."]" ..
|
|
"button_exit[0,0.5;2,1;view_balance;"..S("Balance").."]" ..
|
|
"button_exit[2,0.5;2,1;view_credit;"..S("Credit").."]" ..
|
|
"textlist[0,1.2;13,7;statement_list;"..table.concat(lines, ",").."]"..
|
|
"label[0,8.4;"..current_total_label.."]"..
|
|
"button_exit[11,8.4;2,1;back;"..S("Back").."]"
|
|
|
|
minetest.show_formspec(player_name, form_name, formspec)
|
|
end
|
|
|
|
-- Shows the main menu form for the ATM.
|
|
function main_form(player, pos)
|
|
local player_name = player:get_player_name()
|
|
local data = bank_accounts.get_account_data(player_name)
|
|
local next_rate = 0
|
|
if data.credit > 0 then
|
|
next_rate = math.max(1, math.floor(data.credit * credit_rate))
|
|
end
|
|
|
|
minetest.show_formspec(player_name, "bank_accounts:atm_options",
|
|
"size[8,8]" ..
|
|
"button_exit[1,.5;2,1;withdrawal;"..S("Withdraw").."]" ..
|
|
"button_exit[1,1.5;2,1;deposit;"..S("Deposit").."]" ..
|
|
"button_exit[1,2.5;3,1;pay_credit;"..S("Pay Credit Debt").."]" ..
|
|
"label[4.5,0.8;"..S("Account Balance: @1", string.format("%.2f", data.balance) .. " MG").."]" ..
|
|
"label[4.5,1.3;"..S("Total Credit Debt: @1", string.format("%.2f", data.credit) .. " MG").."]" ..
|
|
"label[4.5,1.8;"..S("Next Rate: @1", tostring(next_rate) .. " MG").."]" ..
|
|
"button_exit[4.5,2.5;3,1;statement;"..S("Account Statement").."]" ..
|
|
"button_exit[1,3.5;3,1;credit_card;"..S("Get Credit Card").."]" ..
|
|
"button_exit[1,4.5;3,1;debit_card;"..S("Get Debit Card").."]" ..
|
|
"button_exit[5,7;2,1;exit;"..S("Close").."]")
|
|
end
|
|
|
|
-- Node definition for the ATM
|
|
minetest.register_node("bank_accounts:atm", {
|
|
description = S("Automatic Teller Machine"),
|
|
drawtype = "mesh",
|
|
mesh = "atm.obj",
|
|
paramtype = "light",
|
|
paramtype2 = "facedir",
|
|
tiles = {"atm_col.png"},
|
|
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
|
|
|
|
-- Create inventories when the node is placed in the world.
|
|
on_construct = function(pos)
|
|
local meta = minetest.get_meta(pos)
|
|
local inv = meta:get_inventory()
|
|
inv:set_size("ones", 1)
|
|
inv:set_size("fives", 1)
|
|
inv:set_size("tens", 1)
|
|
inv:set_size("withdrawal", 3)
|
|
end,
|
|
|
|
-- Called when a player right-clicks the node.
|
|
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
|
|
pos_info = pos -- Set the global position for other functions to use.
|
|
if itemstack:get_name() ~= "bank_accounts:atm_card" then
|
|
minetest.chat_send_player(player:get_player_name(), S("[ATM] Must use ATM card."))
|
|
return
|
|
end
|
|
-- Show the initial PIN entry form.
|
|
minetest.show_formspec(player:get_player_name(), "bank_accounts:atm_home",
|
|
"size[8,8]" ..
|
|
"pwdfield[2,4;4,1;fourdigitpin;"..S("Four Digit Pin:").."]" ..
|
|
"button_exit[5,6;2,1;enter;"..S("Enter").."]" ..
|
|
"button_exit[3,6;2,1;exit;"..S("Cancel").."]")
|
|
end,
|
|
|
|
-- Prevents players from putting incorrect items into the deposit slots.
|
|
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
|
|
if listname == "ones" and stack:get_name() ~= "currency:minegeld" then return 0 end
|
|
if listname == "fives" and stack:get_name() ~= "currency:minegeld_5" then return 0 end
|
|
if listname == "tens" and stack:get_name() ~= "currency:minegeld_10" then return 0 end
|
|
return stack:get_count()
|
|
end,
|
|
|
|
-- Prevents players from moving items between the typed deposit slots.
|
|
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
|
|
local inv = minetest.get_meta(pos):get_inventory()
|
|
local stack = inv:get_stack(from_list, from_index)
|
|
if to_list == "ones" and stack:get_name() ~= "currency:minegeld" then return 0 end
|
|
if to_list == "fives" and stack:get_name() ~= "currency:minegeld_5" then return 0 end
|
|
if to_list == "tens" and stack:get_name() ~= "currency:minegeld_10" then return 0 end
|
|
return count
|
|
end
|
|
})
|
|
|
|
-- Central handler for all formspec interactions related to the ATM.
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
-- Only process forms belonging to this node.
|
|
if not formname:find("bank_accounts:atm") then
|
|
return
|
|
end
|
|
|
|
local player_name = player:get_player_name()
|
|
local pos = pos_info
|
|
if not pos then
|
|
return
|
|
end
|
|
|
|
-- Handle PIN entry form.
|
|
if formname == "bank_accounts:atm_home" then
|
|
if fields.enter then
|
|
if bank_accounts.get_pin(player_name) == fields.fourdigitpin then
|
|
main_form(player, pos)
|
|
else
|
|
minetest.chat_send_player(player_name, S("[ATM] Invalid Pin."))
|
|
end
|
|
end
|
|
|
|
-- Handle main menu options.
|
|
elseif formname == "bank_accounts:atm_options" then
|
|
if fields.withdrawal then
|
|
minetest.get_meta(pos):get_inventory():set_size("withdrawal", 3)
|
|
minetest.show_formspec(player_name, "bank_accounts:atm_withdrawal",
|
|
"size[8,8]" ..
|
|
"field[2,4;5,1;money;"..S("Amount:")..";]" ..
|
|
"button_exit[3,6;2,1;exit;"..S("Cancel").."]" ..
|
|
"button_exit[5,6;2,1;enter;"..S("Enter").."]")
|
|
elseif fields.deposit then
|
|
local inv = minetest.get_meta(pos):get_inventory()
|
|
inv:set_size("ones", 1); inv:set_size("fives", 1); inv:set_size("tens", 1)
|
|
local list_name = "nodemeta:" .. pos.x .. "," .. pos.y .. "," .. pos.z
|
|
minetest.show_formspec(player_name, "bank_accounts:atm_deposit",
|
|
"size[8,8]" ..
|
|
"label[1,.5;1 MG]" .. "label[2,.5;5 MG]" .. "label[3,.5;10 MG]" ..
|
|
"list["..list_name..";ones;.75,1;1,1]" ..
|
|
"list["..list_name..";fives;1.75,1;1,1]" ..
|
|
"list["..list_name..";tens;2.75,1;1,1]" ..
|
|
"list[current_player;main;0,3;8,4;]" ..
|
|
"button_exit[3,7;2,1;exit;"..S("Cancel").."]" ..
|
|
"button_exit[5,7;2,1;enter;"..S("Enter").."]")
|
|
elseif fields.pay_credit then
|
|
local credit_debt = bank_accounts.get_credit(player_name)
|
|
local next_rate = 0
|
|
if credit_debt > 0 then
|
|
next_rate = math.max(1, math.floor(credit_debt * credit_rate))
|
|
end
|
|
minetest.show_formspec(player_name, "bank_accounts:atm_pay_credit",
|
|
"size[8,8]" ..
|
|
"label[0,0.5;"..S("Total Credit Debt: @1", string.format("%.2f", credit_debt).." MG").."]"..
|
|
"label[0,2;"..S("Next calculated rate:").."]"..
|
|
"button_exit[0,2.5;3.5,1;pay_rate;"..S("Pay Next Rate (@1 MG)", next_rate).."]" ..
|
|
"label[4.2,2;"..S("Or enter a custom amount:").."]" ..
|
|
"field[4.5,2.8;3.5,1;custom_amount;"..""..";]" ..
|
|
"button_exit[4.2,3.5;3.5,1;pay_custom;"..S("Pay Custom Amount").."]" ..
|
|
"button_exit[3,7;2,1;exit;"..S("Cancel").."]")
|
|
elseif fields.statement then
|
|
show_atm_statement_form(player, "balance")
|
|
elseif fields.credit_card then
|
|
player:get_inventory():add_item("main", "bank_accounts:credit_card")
|
|
main_form(player, pos)
|
|
elseif fields.debit_card then
|
|
player:get_inventory():add_item("main", "bank_accounts:debit_card")
|
|
main_form(player, pos)
|
|
end
|
|
|
|
-- Handle withdrawal form.
|
|
elseif formname == "bank_accounts:atm_withdrawal" then
|
|
if fields.enter then
|
|
local amount = tonumber(fields.money)
|
|
if amount and amount > 0 and amount % 1 == 0 and bank_accounts.get_balance(player_name) >= amount then
|
|
bank_accounts.add_balance(player_name, -amount, "ATM Withdrawal", "", "")
|
|
local tens=math.floor(amount/10); local rem=amount%10; local fives=math.floor(rem/5); local ones=rem%5
|
|
local inv = minetest.get_meta(pos):get_inventory()
|
|
inv:set_stack("withdrawal", 1, {name="currency:minegeld_10", count=tens})
|
|
inv:set_stack("withdrawal", 2, {name="currency:minegeld_5", count=fives})
|
|
inv:set_stack("withdrawal", 3, {name="currency:minegeld", count=ones})
|
|
local list_name = "nodemeta:"..pos.x..","..pos.y..","..pos.z
|
|
minetest.show_formspec(player_name, "bank_accounts:withdrawn_money",
|
|
"size[8,8]" ..
|
|
"label[1,0.5;"..S("Please take your money.").. "]" ..
|
|
"list["..list_name..";withdrawal;1,1;3,1]" ..
|
|
"list[current_player;main;0,3;8,4;]" ..
|
|
"button_exit[3,7;2,1;exit;"..S("Close").."]")
|
|
else
|
|
minetest.chat_send_player(player_name, S("[ATM] Insufficient funds or invalid amount."))
|
|
main_form(player, pos)
|
|
end
|
|
else
|
|
main_form(player, pos)
|
|
end
|
|
|
|
-- Handle deposit form.
|
|
elseif formname == "bank_accounts:atm_deposit" then
|
|
local inv = minetest.get_meta(pos):get_inventory()
|
|
if fields.enter then
|
|
local total_deposit = inv:get_stack("ones",1):get_count() + inv:get_stack("fives",1):get_count()*5 + inv:get_stack("tens",1):get_count()*10
|
|
if total_deposit > 0 then
|
|
bank_accounts.add_balance(player_name, total_deposit, "ATM Deposit", "", "")
|
|
minetest.chat_send_player(player_name, S("[ATM] Deposited @1.", tostring(total_deposit).." MG"))
|
|
end
|
|
else
|
|
-- Return items to player if they cancel.
|
|
local player_inv = player:get_inventory()
|
|
player_inv:add_item("main", inv:get_stack("ones",1))
|
|
player_inv:add_item("main", inv:get_stack("fives",1))
|
|
player_inv:add_item("main", inv:get_stack("tens",1))
|
|
end
|
|
inv:set_stack("ones",1,nil); inv:set_stack("fives",1,nil); inv:set_stack("tens",1,nil)
|
|
main_form(player, pos)
|
|
|
|
-- Handle form after taking withdrawn money.
|
|
elseif formname == "bank_accounts:withdrawn_money" then
|
|
main_form(player, pos)
|
|
|
|
-- Handle credit payment form.
|
|
elseif formname == "bank_accounts:atm_pay_credit" then
|
|
local amount_to_pay = 0
|
|
local is_valid = false
|
|
if fields.pay_rate then
|
|
local credit_debt = bank_accounts.get_credit(player_name)
|
|
if credit_debt > 0 then
|
|
amount_to_pay = math.max(1, math.floor(credit_debt * credit_rate))
|
|
end
|
|
is_valid = true
|
|
elseif fields.pay_custom then
|
|
amount_to_pay = normalize_and_tonumber(fields.custom_amount)
|
|
local min_payment = 0
|
|
local credit_debt = bank_accounts.get_credit(player_name)
|
|
if credit_debt > 0 then
|
|
min_payment = math.max(1, math.floor(credit_debt * credit_rate))
|
|
end
|
|
if not amount_to_pay or amount_to_pay < min_payment then
|
|
minetest.chat_send_player(player_name, S("[ATM] You must pay at least the minimum monthly rate."))
|
|
else
|
|
is_valid = true
|
|
end
|
|
end
|
|
|
|
if is_valid and amount_to_pay and amount_to_pay > 0 then
|
|
if bank_accounts.get_balance(player_name) < amount_to_pay then
|
|
minetest.chat_send_player(player_name, S("[ATM] Insufficient funds."))
|
|
elseif bank_accounts.get_credit(player_name) < amount_to_pay then
|
|
minetest.chat_send_player(player_name, S("[ATM] You don't have that much credit debt."))
|
|
else
|
|
bank_accounts.add_balance(player_name, -amount_to_pay, "Rate Payment", "", "")
|
|
bank_accounts.add_credit(player_name, -amount_to_pay, "Rate Payment", "", "")
|
|
minetest.chat_send_player(player_name, S("[ATM] Paid @1 of credit debt.", string.format("%.2f", amount_to_pay).." MG"))
|
|
end
|
|
elseif fields.pay_custom and is_valid == false then
|
|
-- Do nothing, error message was already sent.
|
|
elseif not fields.exit and not fields.quit and (fields.pay_rate or fields.pay_custom) then
|
|
-- Catch other invalid inputs.
|
|
minetest.chat_send_player(player_name, S("[ATM] Invalid amount."))
|
|
end
|
|
main_form(player, pos)
|
|
|
|
-- Handle statement form.
|
|
elseif formname:find("bank_accounts:atm_statement@") then
|
|
local target_name = formname:match("bank_accounts:atm_statement@(.*)")
|
|
if not target_name then return end
|
|
if fields.back then
|
|
main_form(player, pos)
|
|
else
|
|
show_atm_statement_form(player, fields.view_credit and "credit" or "balance")
|
|
end
|
|
end
|
|
end)
|