bank_accounts/atm.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)