Commit c25165f37a84696753be1a51322bf3357ba08b43

Authored by DarkGod
2 parents b867ca1a c0eea232

Merge branch 'microtxn'

Showing 32 changed files with 1039 additions and 67 deletions
  1 +-- TE4 - T-Engine 4
  2 +-- Copyright (C) 2009 - 2017 Nicolas Casalini
  3 +--
  4 +-- This program is free software: you can redistribute it and/or modify
  5 +-- it under the terms of the GNU General Public License as published by
  6 +-- the Free Software Foundation, either version 3 of the License, or
  7 +-- (at your option) any later version.
  8 +--
  9 +-- This program is distributed in the hope that it will be useful,
  10 +-- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +-- GNU General Public License for more details.
  13 +--
  14 +-- You should have received a copy of the GNU General Public License
  15 +-- along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +--
  17 +-- Nicolas Casalini "DarkGod"
  18 +-- darkgod@te4.org
  19 +
  20 +require "engine.class"
  21 +
  22 +module(..., package.seeall, class.make)
  23 +
  24 +function _M:init()
  25 + self.cart = {}
  26 +end
  27 +
  28 +function _M:addItem(id)
  29 + self.cart[#self.cart+1] = { name="Test", desc="Test desc", kind="cosmetic", cost=100 }
  30 +end
  31 +
  32 +function _M:purchase()
  33 +
  34 +end
... ...
... ... @@ -483,6 +483,66 @@ function _M:addonMD5(add, base)
483 483 return fmd5
484 484 end
485 485
  486 +function _M:loadAddon(mod, add, hashlist, hooks_list)
  487 + add.version_name = ("%s-%s-%d.%d.%d"):format(mod.short_name, add.short_name, add.version[1], add.version[2], add.version[3])
  488 +
  489 + print("Binding addon", add.long_name, add.teaa, add.version_name)
  490 + local base
  491 + if add.teaa then
  492 + base = fs.getRealPath(add.teaa)
  493 + else
  494 + base = fs.getRealPath(add.dir)
  495 + end
  496 +
  497 + if add.data then
  498 + print(" * with data")
  499 + if add.teaac then fs.mount("subdir:/"..add.teaac.."/data/|"..fs.getRealPath(add.teaa), "/data-"..add.short_name, true)
  500 + elseif add.teaa then fs.mount("subdir:/data/|"..fs.getRealPath(add.teaa), "/data-"..add.short_name, true)
  501 + else fs.mount(base.."/data", "/data-"..add.short_name, true)
  502 + end
  503 + end
  504 + if add.superload then
  505 + print(" * with superload")
  506 + if add.teaac then fs.mount("subdir:/"..add.teaac.."/superload/|"..fs.getRealPath(add.teaa), "/mod/addons/"..add.short_name.."/superload", true)
  507 + elseif add.teaa then fs.mount("subdir:/superload/|"..fs.getRealPath(add.teaa), "/mod/addons/"..add.short_name.."/superload", true)
  508 + else fs.mount(base.."/superload", "/mod/addons/"..add.short_name.."/superload", true)
  509 + end
  510 +
  511 + table.insert(_G.__addons_superload_order, add.short_name)
  512 + end
  513 + if add.overload then
  514 + print(" * with overload")
  515 + if add.teaac then fs.mount("subdir:/"..add.teaac.."/overload/|"..fs.getRealPath(add.teaa), "/", false)
  516 + elseif add.teaa then fs.mount("subdir:/overload/|"..fs.getRealPath(add.teaa), "/", false)
  517 + else fs.mount(base.."/overload", "/", false)
  518 + end
  519 + end
  520 + if add.hooks then
  521 + if add.teaac then fs.mount("subdir:/"..add.teaac.."/hooks/|"..fs.getRealPath(add.teaa), "/hooks/"..add.short_name, true)
  522 + elseif add.teaa then fs.mount("subdir:/hooks/|"..fs.getRealPath(add.teaa), "/hooks/"..add.short_name, true)
  523 + else fs.mount(base.."/hooks", "/hooks/"..add.short_name, true)
  524 + end
  525 +
  526 + hooks_list[#hooks_list+1] = "/hooks/"..add.short_name
  527 + print(" * with hooks")
  528 + end
  529 +
  530 + -- Compute addon md5
  531 + local hash_valid, hash_err
  532 + if config.settings.cheat and not __module_extra_info.compute_md5_only then
  533 + hash_valid, hash_err = false, "cheat mode skipping addon validation"
  534 + else
  535 + local fmd5 = self:addonMD5(add)
  536 + hashlist[#hashlist+1] = {module=mod.short_name, addon=add.version_name, md5=fmd5}
  537 +-- hash_valid, hash_err = profile:checkAddonHash(mod.short_name, add.version_name, fmd5)
  538 + end
  539 +
  540 +-- if hash_err then hash_err = hash_err .. " [addon: "..add.short_name.."]" end
  541 +-- add.hash_valid, add.hash_err = hash_valid, hash_err
  542 +
  543 + mod.addons[add.short_name] = add
  544 +end
  545 +
486 546 function _M:loadAddons(mod, saveuse)
487 547 local hashlist = {}
488 548 local adds = self:listAddons(mod, true)
... ... @@ -601,63 +661,7 @@ You may try to force loading if you are sure the savefile does not use that addo
601 661 mod.addons = {}
602 662 _G.__addons_superload_order = {}
603 663 for i, add in ipairs(adds) do
604   - add.version_name = ("%s-%s-%d.%d.%d"):format(mod.short_name, add.short_name, add.version[1], add.version[2], add.version[3])
605   -
606   - print("Binding addon", add.long_name, add.teaa, add.version_name)
607   - local base
608   - if add.teaa then
609   - base = fs.getRealPath(add.teaa)
610   - else
611   - base = fs.getRealPath(add.dir)
612   - end
613   -
614   - if add.data then
615   - print(" * with data")
616   - if add.teaac then fs.mount("subdir:/"..add.teaac.."/data/|"..fs.getRealPath(add.teaa), "/data-"..add.short_name, true)
617   - elseif add.teaa then fs.mount("subdir:/data/|"..fs.getRealPath(add.teaa), "/data-"..add.short_name, true)
618   - else fs.mount(base.."/data", "/data-"..add.short_name, true)
619   - end
620   - end
621   - if add.superload then
622   - print(" * with superload")
623   - if add.teaac then fs.mount("subdir:/"..add.teaac.."/superload/|"..fs.getRealPath(add.teaa), "/mod/addons/"..add.short_name.."/superload", true)
624   - elseif add.teaa then fs.mount("subdir:/superload/|"..fs.getRealPath(add.teaa), "/mod/addons/"..add.short_name.."/superload", true)
625   - else fs.mount(base.."/superload", "/mod/addons/"..add.short_name.."/superload", true)
626   - end
627   -
628   - table.insert(_G.__addons_superload_order, add.short_name)
629   - end
630   - if add.overload then
631   - print(" * with overload")
632   - if add.teaac then fs.mount("subdir:/"..add.teaac.."/overload/|"..fs.getRealPath(add.teaa), "/", false)
633   - elseif add.teaa then fs.mount("subdir:/overload/|"..fs.getRealPath(add.teaa), "/", false)
634   - else fs.mount(base.."/overload", "/", false)
635   - end
636   - end
637   - if add.hooks then
638   - if add.teaac then fs.mount("subdir:/"..add.teaac.."/hooks/|"..fs.getRealPath(add.teaa), "/hooks/"..add.short_name, true)
639   - elseif add.teaa then fs.mount("subdir:/hooks/|"..fs.getRealPath(add.teaa), "/hooks/"..add.short_name, true)
640   - else fs.mount(base.."/hooks", "/hooks/"..add.short_name, true)
641   - end
642   -
643   - hooks_list[#hooks_list+1] = "/hooks/"..add.short_name
644   - print(" * with hooks")
645   - end
646   -
647   - -- Compute addon md5
648   - local hash_valid, hash_err
649   - if config.settings.cheat and not __module_extra_info.compute_md5_only then
650   - hash_valid, hash_err = false, "cheat mode skipping addon validation"
651   - else
652   - local fmd5 = self:addonMD5(add)
653   - hashlist[#hashlist+1] = {module=mod.short_name, addon=add.version_name, md5=fmd5}
654   --- hash_valid, hash_err = profile:checkAddonHash(mod.short_name, add.version_name, fmd5)
655   - end
656   -
657   --- if hash_err then hash_err = hash_err .. " [addon: "..add.short_name.."]" end
658   --- add.hash_valid, add.hash_err = hash_valid, hash_err
659   -
660   - mod.addons[add.short_name] = add
  664 + self:loadAddon(mod, add, hashlist, hooks_list)
661 665 end
662 666
663 667 -- We load hooks at the end of all superloads and overloads
... ...
... ... @@ -77,6 +77,7 @@ function _M:init()
77 77 self.chat = UserChat.new()
78 78 self.dlc_files = {classes={}, files={}}
79 79 self.saved_events = {}
  80 + self.temporary_event_handlers = {}
80 81 self.generic = {}
81 82 self.modules = {}
82 83 self.evt_cbs = {}
... ... @@ -650,11 +651,20 @@ function _M:eventFunFacts(e)
650 651 end
651 652 end
652 653
  654 +function _M:registerTemporaryEventHandler(name, fct)
  655 + self.temporary_event_handlers[name] = self.temporary_event_handlers[name] or {}
  656 + table.insert(self.temporary_event_handlers[name], fct)
  657 +end
  658 +
653 659 --- Got an event from the profile thread
654 660 function _M:handleEvent(e)
655 661 if type(e) == "string" then e = e:unserialize() end
656 662 if not e then return end
657   - if self["event"..e.e] then self["event"..e.e](self, e) end
  663 + if self["event"..e.e] then self["event"..e.e](self, e)
  664 + elseif self.temporary_event_handlers[e.e] then
  665 + for _, fct in ipairs(self.temporary_event_handlers[e.e]) do print("[PROFILE] temporary_event_handlers", e.e, pcall(fct, e)) end
  666 + self.temporary_event_handlers[e.e] = nil
  667 + end
658 668 return e
659 669 end
660 670
... ...
... ... @@ -399,7 +399,7 @@ function _M:loadHooksFile(file)
399 399 local f, err = loadfile(_current_hook_dir.."/"..file)
400 400 if not f and err then error(err) end
401 401 setfenv(f, setmetatable({}, {__index = _G}))
402   - f()
  402 + return f()
403 403 end
404 404
405 405 function _M:bindHook(hook, fct)
... ...
... ... @@ -49,7 +49,7 @@ function _M:init(t)
49 49 self.custom_calls = t.custom_calls or {}
50 50 if self.allow_login == nil then self.allow_login = true end
51 51
52   - if self.allow_login and self.url:find("^http://te4%.org/") and profile.auth then
  52 + if self.allow_login and self.url:find("^https://te4%.org/") and profile.auth then
53 53 local param = "_te4ah="..profile.auth.hash.."&_te4ad="..profile.auth.drupid
54 54
55 55 local first = self.url:find("?", 1, 1)
... ... @@ -57,7 +57,7 @@ function _M:init(t)
57 57 else self.url = self.url.."?"..param end
58 58 end
59 59
60   - if self.url:find("^http://te4%.org/") then
  60 + if self.url:find("^https://te4%.org/") then
61 61 local param = "_te4"
62 62
63 63 local first = self.url:find("?", 1, 1)
... ...
  1 +-- TE4 - T-Engine 4
  2 +-- Copyright (C) 2009 - 2017 Nicolas Casalini
  3 +--
  4 +-- This program is free software: you can redistribute it and/or modify
  5 +-- it under the terms of the GNU General Public License as published by
  6 +-- the Free Software Foundation, either version 3 of the License, or
  7 +-- (at your option) any later version.
  8 +--
  9 +-- This program is distributed in the hope that it will be useful,
  10 +-- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +-- GNU General Public License for more details.
  13 +--
  14 +-- You should have received a copy of the GNU General Public License
  15 +-- along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +--
  17 +-- Nicolas Casalini "DarkGod"
  18 +-- darkgod@te4.org
  19 +
  20 +require "engine.class"
  21 +local Module = require "engine.Module"
  22 +local Downloader = require "engine.dialogs.Downloader"
  23 +local Entity = require "engine.Entity"
  24 +local Dialog = require "engine.ui.Dialog"
  25 +local Image = require "engine.ui.Image"
  26 +local Textzone = require "engine.ui.Textzone"
  27 +local TextzoneList = require "engine.ui.TextzoneList"
  28 +local ListColumns = require "engine.ui.ListColumns"
  29 +local Button = require "engine.ui.Button"
  30 +local WebView = require "engine.ui.WebView"
  31 +
  32 +module(..., package.seeall, class.inherit(Dialog))
  33 +
  34 +local bonus_vault_slots_text = "#{italic}##UMBER#Bonus vault slots from this order: #ROYAL_BLUE#%d#{normal}#"
  35 +local bonus_vault_slots_tooltip = "For every purchase of #{italic}##GREY#%s#LAST##{normal}# you gain a permanent additional vault slot.\n#GOLD##{italic}#Because why not!#{normal}#"
  36 +
  37 +local coins_balance_text = "#{italic}##UMBER#Voratun Coins available from your donations: #ROYAL_BLUE#%d#{normal}#"
  38 +local coins_balance_tooltip = "For every donations you've ever made you have earned voratun coins. These can be spent purchasing expansions or options on the online store. This is the amount you have left, if your purchase total is below this number you'll instantly get your purchase validated, if not you'll need to donate some more first.\n#GOLD##{italic}#Thanks for your support, every little bit helps the game survive for years on!#{normal}#"
  39 +
  40 +function _M:init(mode)
  41 + if not mode then mode = core.steam and "steam" or "te4" end
  42 + self.mode = mode
  43 +
  44 + self.ui = "microtxn"
  45 +
  46 + self.cart = {}
  47 +
  48 + self.base_title_text = game.__mod_info.long_name.." #GOLD#Online Store#LAST#"
  49 + Dialog.init(self, self.base_title_text, game.w * 0.8, game.h * 0.8)
  50 +
  51 + self.categories_icons = {
  52 + pay2die = Entity.new{image="/data/gfx/mtx/ui/category_pay2die.png"},
  53 + community = Entity.new{image="/data/gfx/mtx/ui/category_community.png"},
  54 + cosmetic = Entity.new{image="/data/gfx/mtx/ui/category_cosmetic.png"},
  55 + misc = Entity.new{image="/data/gfx/mtx/ui/category_misc.png"},
  56 + }
  57 + local in_cart_icon = Entity.new{image="/data/gfx/mtx/ui/in_cart.png"}
  58 +
  59 + self.list = {}
  60 + self.purchasables = {}
  61 + self.recap = {}
  62 +
  63 + self.c_waiter = Textzone.new{auto_width=1, auto_height=1, text="#YELLOW#-- connecting to server... --"}
  64 + self.c_list = ListColumns.new{width=self.iw - 350, height=self.ih, item_height=132, hide_columns=true, scrollbar=true, sortable=true, columns={
  65 + {name="", width=100, display_prop="", direct_draw=function(item, x, y)
  66 + item.img:toScreen(nil, x+2, y+2, 128, 128)
  67 + item.category_img:toScreen(nil, x+2+64+32, y+2+64+32, 32, 32)
  68 + if self.cart[item.id_purchasable] and item.nb_purchase > 0 then in_cart_icon:toScreen(nil, x+2, y+2, 128, 128) end
  69 + item.txt:display(x+10+130, y+2 + (128 - item.txt.h) / 2, 0)
  70 + end},
  71 + }, list=self.list, all_clicks=true, fct=function(item, _, button) self:use(item, button) end, select=function(item, sel) self:onSelectItem(item) end}
  72 + self.c_list.on_focus_change = function(_, v) if not v then game:tooltipHide() end end
  73 +
  74 + self.c_bonus_vault_slots = Textzone.new{has_box=true, width=340, auto_height=1, text=bonus_vault_slots_text:format(0), can_focus=true}
  75 + self.c_bonus_vault_slots.on_focus_change = function(_, v)
  76 + if v then
  77 + local txt = self:getUIElement(self.c_bonus_vault_slots)
  78 + game:tooltipDisplayAtMap(txt.x, txt.y, (bonus_vault_slots_tooltip):format(self:currencyDisplay(2)))
  79 + else
  80 + game:tooltipHide()
  81 + end
  82 + end
  83 +
  84 + self.c_coins_available = Textzone.new{has_box=true, width=340, auto_height=1, text=coins_balance_text:format(0), can_focus=true}
  85 + self.c_coins_available.on_focus_change = function(_, v)
  86 + if v then
  87 + local txt = self:getUIElement(self.c_coins_available)
  88 + game:tooltipDisplayAtMap(txt.x, txt.y, (coins_balance_tooltip):format())
  89 + else
  90 + game:tooltipHide()
  91 + end
  92 + end
  93 +
  94 + self.c_do_purchase = Button.new{text="Purchase", fct=function() self:doPurchase() end}
  95 +
  96 + self.c_recap = ListColumns.new{width=350, height=self.ih - self.c_do_purchase.h - math.max(self.c_bonus_vault_slots.h, self.c_coins_available.h), scrollbar=true, columns={
  97 + {name="Name", width=50, display_prop="recap_name"},
  98 + {name="Price", width=35, display_prop="recap_price"},
  99 + {name="Qty", width=15, display_prop="recap_qty"},
  100 + }, list=self.recap, all_clicks=true, fct=function(item, _, button)
  101 + if item.total then return end
  102 + if button == "left" then button = "right"
  103 + elseif button == "right" then button = "left" end
  104 + self:use(item.item, button)
  105 + end, select=function(item, sel) end}
  106 +
  107 +-- local wv = WebView.new{width=500,height=500, url='https://i.giphy.com/media/uk9A7Mz8Jv4GI/giphy-downsized.gif'}
  108 +
  109 + local uis = {
  110 + {vcenter=0, hcenter=0, ui=self.c_waiter},
  111 + {left=0, top=0, ui=self.c_list},
  112 + {right=0, top=0, ui=self.c_recap},
  113 + {right=0, bottom=0, ui=self.c_do_purchase},
  114 +-- {left=0, top=0, ui=wv},
  115 + }
  116 + -- Only show those for steam as te4.org purchases require already having a donation up
  117 + if mode == "steam" then
  118 + uis[#uis+1] = {right=0, bottom=self.c_do_purchase, ui=self.c_bonus_vault_slots}
  119 + elseif mode == "te4" then
  120 + uis[#uis+1] = {right=0, bottom=self.c_do_purchase, ui=self.c_coins_available}
  121 + end
  122 +
  123 + self:loadUI(uis)
  124 +
  125 + self:setupUI(false, false)
  126 + self:toggleDisplay(self.c_list, false)
  127 +
  128 + self.key:addBinds{
  129 + ACCEPT = "EXIT",
  130 + EXIT = function()
  131 + game:unregisterDialog(self)
  132 + if on_exit then on_exit() end
  133 + end,
  134 + }
  135 +
  136 + self:checks()
  137 +
  138 + self:generateList()
  139 +end
  140 +
  141 +function _M:checks() game:onTickEnd(function()
  142 + if not profile.auth then
  143 + game:unregisterDialog(self)
  144 + Dialog:simplePopup("Online Store", "You need to be logged in before using the store. Please go back to the main menu and login.")
  145 + return
  146 + end
  147 +
  148 + if self.mode == "steam" then
  149 + if not profile.auth.steamid then
  150 + game:unregisterDialog(self)
  151 + Dialog:yesnoPopup("Online Store", "Steam users need to link their profiles to their steam account. This is very easy in just a few clicks. Once this is done, simply restart the game.", function(ret) if ret then
  152 + util.browserOpenUrl("https://te4.org/user/"..profile.auth.drupid.."/steam", {is_external=true})
  153 + end end, "Let's do it! (Opens in your browser)", "Not now")
  154 + end
  155 + elseif self.mode == "te4" then
  156 + -- Handle me more smoothly
  157 + if profile.auth.donated < 6 then
  158 + game:unregisterDialog(self)
  159 + Dialog:yesnoPopup("Online Store", "The Online Store (and expansions) are only purchasable by players that bought the game. Plaese go have a look at the donation page for more explanations.", function(ret) if ret then
  160 + util.browserOpenUrl("https://te4.org/donate", {is_external=true})
  161 + end end, "Let's go! (Opens in your browser)", "Not now")
  162 + end
  163 + end
  164 +end) end
  165 +
  166 +function _M:onSelectItem(item)
  167 + if self.in_paying_ui then game:tooltipHide() return end
  168 + if not item then return game:tooltipHide() end
  169 +
  170 + if self.cur_sel_item then self.cur_sel_item.txt.pingpong = nil self.cur_sel_item.txt.scrollbar.pos = 0 end
  171 + item.txt.pingpong = 0
  172 +
  173 + if item.last_display_x then
  174 + game:tooltipDisplayAtMap(item.last_display_x + self.c_list.w, item.last_display_y, item.tooltip)
  175 + else
  176 + game:tooltipHide()
  177 + end
  178 + self.cur_sel_item = item
  179 +end
  180 +
  181 +function _M:use(item, button)
  182 + if self.in_paying_ui then return end
  183 + if not item then return end
  184 + if button == "right" then
  185 + item.nb_purchase = math.max(0, item.nb_purchase - 1)
  186 + if item.nb_purchase <= 0 then self.cart[item.id] = nil end
  187 + elseif button == "left" then
  188 + if item.can_multiple then
  189 + item.nb_purchase = item.nb_purchase + 1
  190 + else
  191 + item.nb_purchase = math.min(1, item.nb_purchase + 1)
  192 + end
  193 + self.cart[item.id] = true
  194 + end
  195 +
  196 + self:updateCart()
  197 +end
  198 +
  199 +function _M:currencyDisplay(v)
  200 + if self.user_currency then
  201 + return ("%0.2f %s"):format(v, self.user_currency)
  202 + else
  203 + return ("%d coins"):format(v)
  204 + end
  205 +end
  206 +
  207 +function _M:updateCart()
  208 + local nb_items, total_sum, total_core_sum = 0, 0, 0
  209 + table.empty(self.recap)
  210 +
  211 + for id, ok in pairs(self.cart) do if ok then
  212 + local item = self.purchasables[id]
  213 + nb_items = nb_items + item.nb_purchase
  214 + total_sum = total_sum + item.nb_purchase * item.price
  215 + total_core_sum = total_core_sum + item.nb_purchase * item.core_price
  216 +
  217 + self.recap[#self.recap+1] = {
  218 + sort_name = item.name,
  219 + recap_name = item.img:getDisplayString()..item.name,
  220 + recap_price = self:currencyDisplay(item.price * item.nb_purchase),
  221 + recap_qty = item.nb_purchase,
  222 + item = item,
  223 + }
  224 + end end
  225 + table.sort(self.recap, "sort_name")
  226 + self.recap[#self.recap+1] = {
  227 + recap_name = "#{bold}#TOTAL#{normal}#",
  228 + recap_price = self:currencyDisplay(total_sum),
  229 + recap_qty = nb_items,
  230 + total = true,
  231 + }
  232 +
  233 + self.c_recap:setList(self.recap, true)
  234 + self:updateTitle(self.base_title_text..(" (%d items in cart, %s)"):format(nb_items, self:currencyDisplay(total_sum)))
  235 +
  236 + self:toggleDisplay(self.c_do_purchase, nb_items > 0)
  237 +
  238 + self.c_bonus_vault_slots.text = bonus_vault_slots_text:format(total_core_sum / 20 + (profile.auth.donated % 2) / 2)
  239 + self.c_bonus_vault_slots:generate()
  240 +end
  241 +
  242 +function _M:doPurchase()
  243 + self.in_paying_ui = true
  244 + if core.steam then self:doPurchaseSteam()
  245 + else self:doPurchaseTE4()
  246 + end
  247 +end
  248 +
  249 +function _M:installShimmer(item)
  250 + if not core.webview then
  251 + Dialog:simpleLongPopup(item.name, "In-game browser is inoperant or disabled, impossible to auto-install shimmer pack.\nPlease go to https://te4.org/ to download it manually.", 600)
  252 + return
  253 + end
  254 +
  255 + -- When download is finished, we will try to load the addon dynamically and add it to the current character. We can do taht because cosmetic addons dont require much setup
  256 + local when_done = function()
  257 + local found = false
  258 + local addons = Module:listAddons(game.__mod_info, true)
  259 + for _, add in ipairs(addons) do if add.short_name == "cosmetic-"..item.effect then
  260 + found = true
  261 +
  262 + local hooks_list = {}
  263 + Module:loadAddon(game.__mod_info, add, {}, hooks_list)
  264 +
  265 + dofile("/data/gfx/mtx-shimmers/"..item.effect..".lua")
  266 +
  267 + Dialog:simplePopup(item.name, [[Shimmer pack installed!]])
  268 + break
  269 + end end
  270 +
  271 + if not found then
  272 + Dialog:simpleLongPopup(item.name, [[Could not dynamically link addon to current character, maybe the installation weng wrong.
  273 +You can fix that by manually downloading the shimmer addon from https://te4.org/ and placing it in game/addons/ folder.]], 600)
  274 + end
  275 + end
  276 +
  277 + local co co = coroutine.create(function()
  278 + local filename = ("/addons/%s-cosmetic-%s.teaa"):format(game.__mod_info.short_name, item.effect)
  279 + print("==> downloading", "https://te4.org/download-mtx/"..item.id_purchasable, filename)
  280 + local d = Downloader.new{title="Downloading cosmetic pack: #LIGHT_GREEN#"..item.name, co=co, dest=filename..".tmp", url="https://te4.org/download-mtx/"..item.id_purchasable, allow_downloads={addons=true}}
  281 + local ok = d:start()
  282 + if ok then
  283 + local wdir = fs.getWritePath()
  284 + local _, _, dir, name = filename:find("(.+/)([^/]+)$")
  285 + if dir then
  286 + fs.setWritePath(fs.getRealPath(dir))
  287 + fs.delete(name)
  288 + fs.rename(name..".tmp", name)
  289 + fs.setWritePath(wdir)
  290 +
  291 + when_done()
  292 + end
  293 + end
  294 + end)
  295 + print(coroutine.resume(co))
  296 +end
  297 +
  298 +function _M:paymentSuccess()
  299 + self.in_paying_ui = false
  300 +
  301 + local list = {}
  302 + for id, ok in pairs(self.cart) do if ok then
  303 + local item = self.purchasables[id]
  304 + if item.is_shimmer then
  305 + self:installShimmer(item)
  306 + list[#list+1] = ("- #{bold}##ROYAL_BLUE#%s #SLATE#x%d#WHITE##{normal}#: The pack should be downloading or even finished by now."):format(item.name, item.nb_purchase)
  307 + elseif item.self_event or item.community_event then
  308 + list[#list+1] = ("- #{bold}##ROYAL_BLUE#%s #SLATE#x%d#WHITE##{normal}#: You can now trigger it whenever you are ready."):format(item.name, item.nb_purchase)
  309 + elseif item.effect == "vaultspace" then
  310 + list[#list+1] = ("- #{bold}##ROYAL_BLUE#%s #SLATE#x%d#WHITE##{normal}#: Your available vault space has increased."):format(item.name, item.nb_purchase)
  311 + end
  312 + end end
  313 +
  314 + game:unregisterDialog(self)
  315 + Dialog:simpleLongPopup("Payment", "Payment accepted.\n"..table.concat(list, "\n"), 700)
  316 +end
  317 +
  318 +function _M:paymentFailure()
  319 + self.in_paying_ui = false
  320 +end
  321 +
  322 +function _M:paymentCancel()
  323 + self.in_paying_ui = false
  324 +end
  325 +
  326 +function _M:doPurchaseSteam()
  327 + local popup = Dialog:simplePopup("Connecting to Steam", "Steam Overlay should appear, if it does not please make sure it you have not disabled it.", nil, true)
  328 +
  329 + local cart = {}
  330 + for id, ok in pairs(self.cart) do if ok then
  331 + local item = self.purchasables[id]
  332 + cart[#cart+1] = {
  333 + id_purchasable = id,
  334 + nb_purchase = item.nb_purchase,
  335 + }
  336 + end end
  337 +
  338 + local function onMTXResult(id_cart, ok)
  339 + local finalpopup = Dialog:simplePopup("Connecting to Steam", "Finalizing transaction with Steam servers...", nil, true)
  340 + profile:registerTemporaryEventHandler("MicroTxnSteamFinalizeCartResult", function(e)
  341 + game:unregisterDialog(finalpopup)
  342 + if e.success then
  343 + if e.new_donated then profile.auth.donated = e.new_donated end
  344 + self:paymentSuccess()
  345 + else
  346 + Dialog:simplePopup("Payment", "Payment refused, you have not been billed.")
  347 + self:paymentFailure()
  348 + end
  349 + end)
  350 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='steam_finalize_cart' module=%q store=%q id_cart=%q", game.__mod_info.short_name, "steam", id_cart))
  351 + end
  352 +
  353 + profile:registerTemporaryEventHandler("MicroTxnListCartResult", function(e)
  354 + game:unregisterDialog(popup)
  355 + if e.success then
  356 + core.steam.waitMTXResult(onMTXResult)
  357 + else
  358 + Dialog:simplePopup("Payment", "Payment refused, you have not been billed.")
  359 + self:paymentFailure()
  360 + end
  361 + end)
  362 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='create_cart' module=%q store=%q cart=%q", game.__mod_info.short_name, "steam", table.serialize(cart)))
  363 +end
  364 +
  365 +function _M:doPurchaseTE4()
  366 + local popup = Dialog:simplePopup("Connecting to server", "Please wait...", nil, true)
  367 +
  368 + local cart = {}
  369 + for id, ok in pairs(self.cart) do if ok then
  370 + local item = self.purchasables[id]
  371 + cart[#cart+1] = {
  372 + id_purchasable = id,
  373 + nb_purchase = item.nb_purchase,
  374 + }
  375 + end end
  376 +
  377 + local function finalizePurchase(id_cart)
  378 + local finalpopup = Dialog:simplePopup("Connecting to server", "Please wait...", nil, true)
  379 + profile:registerTemporaryEventHandler("MicroTxnTE4FinalizeCartResult", function(e)
  380 + game:unregisterDialog(finalpopup)
  381 + if e.success then
  382 + if e.new_donated then profile.auth.donated = e.new_donated end
  383 + self:paymentSuccess()
  384 + else
  385 + Dialog:simplePopup("Payment", "Payment refused, you have not been billed.")
  386 + self:paymentFailure()
  387 + end
  388 + end)
  389 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='te4_finalize_cart' module=%q store=%q id_cart=%q", game.__mod_info.short_name, "te4", id_cart))
  390 + end
  391 +
  392 + profile:registerTemporaryEventHandler("MicroTxnListCartResult", function(e)
  393 + game:unregisterDialog(popup)
  394 + if e.success and e.info then
  395 + if e.info:prefix("instant_buy:") then
  396 + local id_cart = tonumber(e.info:sub(13))
  397 + Dialog:yesnoPopup("Online Store", "You have enough coins to instantly purchase those options. Confirm?", function(ret) if ret then
  398 + finalizePurchase(id_cart)
  399 + end end, "Purchase", "Cancel")
  400 + elseif e.info:prefix("requires:") then
  401 + local more = tonumber(e.info:sub(10))
  402 + Dialog:yesnoPopup("Online Store", "You need "..more.." more coins to purchase those options. Do you want to go to the donation page now?", function(ret) if ret then
  403 + util.browserOpenUrl("https://te4.org/donate", {is_external=true})
  404 + end end, "Let's go! (Opens in your browser)", "Not now")
  405 + self:paymentCancel()
  406 + end
  407 + else
  408 + Dialog:simplePopup("Payment", "Payment refused, you have not been billed.")
  409 + self:paymentFailure()
  410 + end
  411 + end)
  412 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='create_cart' module=%q store=%q cart=%q", game.__mod_info.short_name, "te4", table.serialize(cart)))
  413 +end
  414 +
  415 +function _M:buildTooltip(item)
  416 + local text = {}
  417 + if item.community_event then
  418 + text[#text+1] = [[#{bold}##GOLD#Community Online Event#WHITE##{normal}#: Once you have purchased a community event you will be able to trigger it at any later date, on whichever character you choose.
  419 +Community events once triggered will activate for #{bold}#every player currently logged on#{normal}# including yourself. Every player receiving it will know you sent it and thus that you are to thank for it.
  420 +To activate it you will need to have your online events option set to "all" (which is the default value).]]
  421 + end
  422 + if item.self_event then
  423 + text[#text+1] = [[#{bold}##GOLD#Event#WHITE##{normal}#: Once you have purchased an event you will be able to trigger it at any later date, on whichever character you choose.
  424 +To activate it you will need to have your online events option set to "all" (which is the default value).]]
  425 + end
  426 + if item.non_immediate then
  427 + text[#text+1] = [[#{bold}##GOLD#Non Immediate#WHITE##{normal}#: This events adds new content that you have to find by exploration. If you die before finding it, there can be no refunds.]]
  428 + end
  429 + if item.once_per_character then
  430 + text[#text+1] = [[#{bold}##GOLD#Once per Character#WHITE##{normal}#: This event can only be received #{bold}#once per character#{normal}#. Usualy because it adds a new zone or effect to the game that would not make sense to duplicate.]]
  431 + end
  432 + if item.is_shimmer then
  433 + text[#text+1] = [[#{bold}##GOLD#Shimmer Pack#WHITE##{normal}#: Once purchased the game will automatically install the shimmer pack to your game and enable it for your current character too (you will still need to use the Mirror of Reflection to switch them on).
  434 +#LIGHT_GREEN#Bonus perk:#LAST# purchasing any shimmer pack will also give your characters a portable Mirror of Reflection to be able to change your appearance anywhere, anytime!]]
  435 + end
  436 + if item.effect == "vaultspace" then
  437 + text[#text+1] = [[#{bold}##GOLD#Vault Space#WHITE##{normal}#: Once purchased your vault space is permanently increased.]]
  438 + end
  439 + return table.concat(text, '\n')
  440 +end
  441 +
  442 +function _M:generateList()
  443 + profile:registerTemporaryEventHandler("MicroTxnListPurchasables", function(e)
  444 + if e.error then
  445 + Dialog:simplePopup("Online Store", e.error:capitalize())
  446 + game:unregisterDialog(self)
  447 + return
  448 + end
  449 +
  450 + if not e.data then return end
  451 + e.data = e.data:unserialize()
  452 +
  453 + if e.data.infos.steam then
  454 + self.user_country = e.data.infos.steam.country
  455 + self.user_currency = e.data.infos.steam.currency
  456 + end
  457 +
  458 + local list = {}
  459 + for _, res in ipairs(e.data.list) do
  460 + res.id_purchasable = res.id
  461 + res.nb_purchase = 0
  462 + res.img = Entity.new{image=res.image}
  463 + res.category_img = self.categories_icons[res.category or "misc"] or self.categories_icons.misc
  464 + res.txt = TextzoneList.new{width=self.iw - 10 - 132 - 350, height=128, pingpong=20, scrollbar=true}
  465 + res.txt:switchItem(true, ("%s (%s)\n#SLATE##{italic}#%s#{normal}#"):format(res.name, self:currencyDisplay(res.price), res.desc))
  466 + res.txt.pingpong = nil
  467 + res.tooltip = self:buildTooltip(res)
  468 + list[#list+1] = res
  469 + self.purchasables[res.id] = res
  470 + end
  471 + self.list = list
  472 + self.c_list:setList(list)
  473 + self:toggleDisplay(self.c_list, true)
  474 + self:toggleDisplay(self.c_waiter, false)
  475 + self:setFocus(self.c_list)
  476 +
  477 + self.c_coins_available.text = coins_balance_text:format((e.data.infos.balance or 0) * 10)
  478 + self.c_coins_available:generate()
  479 + self.cur_coins_left = e.data.infos.balance or 0
  480 + end)
  481 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='list_purchasables' module=%q store=%q", game.__mod_info.short_name, core.steam and "steam" or "te4"))
  482 +end
... ...
  1 +-- TE4 - T-Engine 4
  2 +-- Copyright (C) 2009 - 2017 Nicolas Casalini
  3 +--
  4 +-- This program is free software: you can redistribute it and/or modify
  5 +-- it under the terms of the GNU General Public License as published by
  6 +-- the Free Software Foundation, either version 3 of the License, or
  7 +-- (at your option) any later version.
  8 +--
  9 +-- This program is distributed in the hope that it will be useful,
  10 +-- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +-- GNU General Public License for more details.
  13 +--
  14 +-- You should have received a copy of the GNU General Public License
  15 +-- along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +--
  17 +-- Nicolas Casalini "DarkGod"
  18 +-- darkgod@te4.org
  19 +
  20 +require "engine.class"
  21 +local Entity = require "engine.Entity"
  22 +local Dialog = require "engine.ui.Dialog"
  23 +local Image = require "engine.ui.Image"
  24 +local Textzone = require "engine.ui.Textzone"
  25 +local ListColumns = require "engine.ui.ListColumns"
  26 +local Button = require "engine.ui.Button"
  27 +
  28 +module(..., package.seeall, class.inherit(Dialog))
  29 +
  30 +function _M:init(mode)
  31 + if not mode then mode = core.steam and "steam" or "te4" end
  32 + self.mode = mode
  33 +
  34 + self.cart = {}
  35 +
  36 + self.base_title_text = game.__mod_info.long_name.." #GOLD#Purchased Options#LAST#"
  37 + Dialog.init(self, self.base_title_text, 600, game.h * 0.8)
  38 +
  39 + self.categories_icons = {
  40 + pay2die = Entity.new{image="/data/gfx/mtx/ui/category_pay2die.png"},
  41 + community = Entity.new{image="/data/gfx/mtx/ui/category_community.png"},
  42 + cosmetic = Entity.new{image="/data/gfx/mtx/ui/category_cosmetic.png"},
  43 + misc = Entity.new{image="/data/gfx/mtx/ui/category_misc.png"},
  44 + }
  45 + local in_cart_icon = Entity.new{image="/data/gfx/mtx/ui/in_cart.png"},
  46 +
  47 + self:generateList()
  48 +
  49 + self.c_waiter = Textzone.new{auto_width=1, auto_height=1, text="#YELLOW#-- connecting to server... --"}
  50 + self.c_list = ListColumns.new{width=self.iw, height=200, scrollbar=true, sortable=true, columns={
  51 + {name="Name", width=80, display_prop="display_name"},
  52 + {name="Available", width=20, display_prop="nb_available"},
  53 + }, list=self.list, all_clicks=true, fct=function(item, _, button) self:use(item, button) end, select=function(item, sel) end}
  54 +
  55 + self:loadUI{
  56 + {vcenter=0, hcenter=0, ui=self.c_waiter},
  57 + {left=0, top=0, ui=self.c_list},
  58 + }
  59 + self:setupUI(false, true)
  60 + self:toggleDisplay(self.c_list, false)
  61 +
  62 + self.key:addBinds{
  63 + ACCEPT = "EXIT",
  64 + EXIT = function()
  65 + game:unregisterDialog(self)
  66 + if on_exit then on_exit() end
  67 + end,
  68 + }
  69 +end
  70 +
  71 +function _M:use(item, button)
  72 + if not item then return end
  73 +
  74 + if game.zone.wilderness then
  75 + Dialog:simplePopup(item.name, "Please use purchased options when not on the worldmap.")
  76 + return
  77 + end
  78 +
  79 + if item.once_per_character and game.state["has_"..item.effect] then
  80 + Dialog:simplePopup(item.name, "This option may only be used once per character to prevent wasting it.")
  81 + return
  82 + end
  83 +
  84 + if (item.community_event or item.self_event) and not game.state:allowOnlineEvent() then
  85 + Dialog:simpleLongPopup(item.name, [[This option requires you to accept to receive events from the server.
  86 +Either you have the option currently disabled or you are playing a campaign that can not support these kind of events (mainly the Arena).
  87 +Make sure you have #GOLD##{bold}#Allow online events#WHITE##{normal}# in the #GOLD##{bold}#Online#WHITE##{normal}# section of the game options set to "all". You can set it back to your own setting once you have received the event.
  88 +]], 600)
  89 + return
  90 + end
  91 +
  92 + Dialog:yesnoPopup(item.name, ("You are about to use a charge of this option. You currently have %d charges remaining."):format(item.nb_available), function(ret) if ret then
  93 + local popup = Dialog:simplePopup(item.name, "Please wait while contacting the server...", nil, true)
  94 + profile:registerTemporaryEventHandler("MicroTxnUseActionable", function(e)
  95 + game:unregisterDialog(popup)
  96 + game:unregisterDialog(self)
  97 +
  98 + if e.success then
  99 + Dialog:simplePopup(item.name, "The option has been activated.")
  100 + else
  101 + Dialog:simplePopup(item.name, "There was an error from the server: "..tostring(e.error))
  102 + end
  103 + end)
  104 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='use_actionable' module=%q id_purchasable=%d", game.__mod_info.short_name, item.id_purchasable))
  105 + end end)
  106 +end
  107 +
  108 +function _M:generateList()
  109 + self.list = {}
  110 +
  111 + profile:registerTemporaryEventHandler("MicroTxnListActionables", function(e)
  112 + if e.error then
  113 + Dialog:simplePopup("Online Store", e.error:capitalize())
  114 + game:unregisterDialog(self)
  115 + return
  116 + end
  117 + if not e.list then return end
  118 + e.list = e.list:unserialize()
  119 + -- table.print(e.list)
  120 +
  121 + local list = {}
  122 + for _, item in ipairs(e.list) do
  123 + item.img = Entity.new{image=item.image}
  124 + item.display_name = item.img:getDisplayString().." "..item.name
  125 + list[#list+1] = item
  126 + end
  127 + self.list = list
  128 + self.c_list:setList(list)
  129 + self:toggleDisplay(self.c_list, true)
  130 + self:toggleDisplay(self.c_waiter, false)
  131 + self:setFocus(self.c_list)
  132 +
  133 + if #list == 0 then
  134 + game:unregisterDialog(self)
  135 + Dialog:yesnoPopup("Online Store", "You have not purchased any usable options yet. Would you like to see the store?", function(ret) if ret then
  136 + package.loaded["engine.dialogs.microtxn.ShowPurchasable"] = nil
  137 + game:registerDialog(require("engine.dialogs.microtxn.ShowPurchasable").new())
  138 + end end)
  139 + end
  140 + end)
  141 + core.profile.pushOrder(string.format("o='MicroTxn' suborder='get_actionables' module=%q", game.__mod_info.short_name))
  142 +end
... ...
... ... @@ -64,7 +64,6 @@ function _M:targetOnTick()
64 64 end
65 65
66 66 --- Display the tooltip, if any
67   -
68 67 function _M:targetDisplayTooltip(dx, dy, force, nb_keyframes)
69 68 -- Tooltip is displayed over all else
70 69 if self.level and self.level.map and self.level.map.finished then
... ... @@ -98,6 +97,11 @@ function _M:tooltipDisplayAtMap(x, y, text, extra, force, nb_keyframes)
98 97 self.tooltip_x = {}
99 98 end
100 99
  100 +--- Forces to hide the tooltip
  101 +function _M:tooltipHide()
  102 + self.tooltip_x = nil
  103 +end
  104 +
101 105 --- Enter/leave targeting mode
102 106 -- This is the "meat" of this interface, do not expect to understand it easily, it mixes some nasty stuff
103 107 -- This require the Game to have both a "key" field (this is the default) and a "normal_key" field<br/>
... ...
... ... @@ -1171,7 +1171,7 @@ end
1171 1171 function fs.iterate(path, filter)
1172 1172 local list = fs.list(path)
1173 1173 if filter then
1174   - if type(filter) == "string" then local fstr = filter filter = function(f) f:find(fstr) return end end
  1174 + if type(filter) == "string" then local fstr = filter filter = function(f) return f:find(fstr) end end
1175 1175 for i = #list, 1, -1 do if not filter(list[i]) then
1176 1176 table.remove(list, i)
1177 1177 end end
... ...
... ... @@ -79,7 +79,7 @@ function _M:init()
79 79 l[#l+1] = {name="Credits", fct=function() game:registerDialog(require("mod.dialogs.Credits").new()) end}
80 80 l[#l+1] = {name="Exit", fct=function() game:onQuit() end}
81 81 if config.settings.cheat then l[#l+1] = {name="Reboot", fct=function() util.showMainMenu() end} end
82   --- if config.settings.cheat then l[#l+1] = {name="webtest", fct=function() util.browserOpenUrl("http://google.com") end} end
  82 + -- if config.settings.cheat then l[#l+1] = {name="webtest", fct=function() util.browserOpenUrl("http://google.com") end} end
83 83 -- if config.settings.cheat then l[#l+1] = {name="webtest", fct=function() util.browserOpenUrl("asset://te4/html/test.html") end} end
84 84
85 85 self.c_background = Checkbox.new{title="Disable animated background", default=config.settings.boot_menu_background and true or false, on_change=function() self:switchBackground() end}
... ...
... ... @@ -35,6 +35,7 @@ require "mod.class.interface.Combat"
35 35 require "mod.class.interface.Archery"
36 36 require "mod.class.interface.ActorInscriptions"
37 37 require "mod.class.interface.ActorObjectUse"
  38 +local Particles = require "engine.Particles"
38 39 local Faction = require "engine.Faction"
39 40 local Dialog = require "engine.ui.Dialog"
40 41 local Map = require "engine.Map"
... ... @@ -4129,6 +4130,24 @@ end
4129 4130 function _M:updateModdableTile()
4130 4131 if not self:updateModdableTilePrepare() then return end
4131 4132
  4133 + if self.shimmer_particles_active then
  4134 + for _, p in ipairs(self.shimmer_particles_active) do self:removeParticles(p) end
  4135 + self.shimmer_particles_active = nil
  4136 + end
  4137 +
  4138 + if self.moddable_tile_base_shimmer_particle then
  4139 + self.shimmer_particles_active = self.shimmer_particles_active or {}
  4140 + for _, def in ipairs(self.moddable_tile_base_shimmer_particle) do
  4141 + local args = table.clone(def.args, true)
  4142 + if def.spot then
  4143 + args.x, args.y = self:attachementSpot(def.spot, true)
  4144 + if args.x and args.y and def.spot_offset then args.x, args.y = args.x + def.spot_offset.x, args.y + def.spot_offset.y end
  4145 + end
  4146 + local p = self:addParticles(Particles.new(def.name, 1, args, def.shader))
  4147 + table.insert(self.shimmer_particles_active, p)
  4148 + end
  4149 + end
  4150 +
4132 4151 local base = "player/"..self.moddable_tile:gsub("#sex#", self.female and "female" or "male").."/"
4133 4152
4134 4153 self.image = base..(self.moddable_tile_shadow or "base_shadow_01.png")
... ... @@ -4145,8 +4164,13 @@ function _M:updateModdableTile()
4145 4164 add[#add+1] = {image_alter="sdm", sdm_double="dynamic", image=base..(self.moddable_tile_base or "base_01.png"), shader=def.shader, shader_args=def.shader_args, textures=def.textures, display_h=2, display_y=-1}
4146 4165 end
4147 4166 end
  4167 + if self.moddable_tile_base_shimmer_aura then
  4168 + for _, def in ipairs(self.moddable_tile_base_shimmer_aura) do
  4169 + add[#add+1] = {image_alter="sdm", sdm_double="dynamic", image=base..(self.moddable_tile_base or "base_01.png"), shader=def.shader, shader_args=def.shader_args, textures=def.textures, display_h=2, display_y=-1}
  4170 + end
  4171 + end
4148 4172
4149   - local basebody = self.moddable_tile_base or "base_01.png"
  4173 + local basebody = self.moddable_tile_base_shimmer or self.moddable_tile_base or "base_01.png"
4150 4174 if self.moddable_tile_base_alter then basebody = self:moddable_tile_base_alter(basebody) end
4151 4175 add[#add+1] = {image = base..basebody, auto_tall=1}
4152 4176
... ...
... ... @@ -1997,6 +1997,9 @@ function _M:setupCommands()
1997 1997 print("===============")
1998 1998 end end,
1999 1999 [{"_g","ctrl"}] = function() if config.settings.cheat then
  2000 + package.loaded["engine.dialogs.microtxn.ShowPurchasable"] = nil
  2001 + self:registerDialog(require("engine.dialogs.microtxn.ShowPurchasable").new())
  2002 +do return end
2000 2003 if self.zone.short_name ~= "test" then
2001 2004 self:changeLevel(1, "test")
2002 2005 else
... ... @@ -2015,6 +2018,9 @@ do return end
2015 2018 print(pcall(f))
2016 2019 end end,
2017 2020 [{"_f","ctrl"}] = function() if config.settings.cheat then
  2021 + package.loaded["engine.dialogs.microtxn.UsePurchased"] = nil
  2022 + self:registerDialog(require("engine.dialogs.microtxn.UsePurchased").new())
  2023 +do return end
2018 2024 local m = game.zone:makeEntityByName(game.level, "actor", "NPC_HUMANOID_KROG")
2019 2025 local x, y = util.findFreeGrid(game.player.x, game.player.y, 20, true, {[Map.ACTOR]=true})
2020 2026 if m and x then
... ...
... ... @@ -108,7 +108,7 @@ function _M:unlockShimmer(o)
108 108 end
109 109
110 110 local moddables = {}
111   - for _, p in ipairs{"moddable_tile", "moddable_tile2", "moddable_tile_back", "moddable_tile_hood", "moddable_tile_particle", "moddable_tile_ornament"} do
  111 + for _, p in ipairs{"moddable_tile", "moddable_tile2", "moddable_tile_back", "moddable_tile_hood", "moddable_tile_particle", "moddable_tile_ornament", "moddable_tile_projectile"} do
112 112 if o[p] then moddables[p] = o[p] end
113 113 end
114 114
... ...
... ... @@ -801,7 +801,7 @@ end
801 801
802 802 function _M:archeryDefaultProjectileVisual(weapon, ammo)
803 803 if (ammo and ammo.proj_image) or (weapon and weapon.proj_image) then
804   - return {display=' ', particle="arrow", particle_args={tile="shockbolt/"..(ammo.proj_image or weapon.proj_image):gsub("%.png$", "")}}
  804 + return {display=' ', particle="arrow", particle_args={tile="shockbolt/"..((ammo.shimmer_moddable and ammo.shimmer_moddable.moddable_tile_projectile) or ammo.proj_image or weapon.proj_image):gsub("%.png$", "")}}
805 805 else
806 806 return {display='/'}
807 807 end
... ...
... ... @@ -25,6 +25,14 @@ local function shimmer(player, slot)
25 25 end
26 26 end
27 27
  28 +local function shimmer_other(player, slot)
  29 + return function()
  30 + package.loaded['mod.dialogs.ShimmerOther'] = nil
  31 + local d = require("mod.dialogs.ShimmerOther").new(player, slot)
  32 + game:registerDialog(d)
  33 + end
  34 +end
  35 +
28 36 local answers = {}
29 37
30 38 for slot, inven in pairs(player.inven) do
... ... @@ -35,6 +43,20 @@ for slot, inven in pairs(player.inven) do
35 43 end
36 44 end
37 45 end
  46 +if world.unlocked_shimmers and world.unlocked_shimmers.SHIMMER_DOLL then
  47 + answers[#answers+1] = {"[Alter the appearance of your body]", action=shimmer_other(player, "SHIMMER_DOLL"), jump="welcome"}
  48 +end
  49 +if world.unlocked_shimmers and world.unlocked_shimmers.SHIMMER_FACIAL then
  50 + answers[#answers+1] = {"[Alter the appearance of your facial features]", action=shimmer_other(player, "SHIMMER_FACIAL"), jump="welcome"}
  51 +end
  52 +if world.unlocked_shimmers and world.unlocked_shimmers.SHIMMER_HAIR then
  53 + answers[#answers+1] = {"[Alter the appearance of your hair]", action=shimmer_other(player, "SHIMMER_HAIR"), jump="welcome"}
  54 +end
  55 +if world.unlocked_shimmers and world.unlocked_shimmers.SHIMMER_AURA then
  56 + answers[#answers+1] = {"[Alter the appearance of your aura]", action=shimmer_other(player, "SHIMMER_AURA"), jump="welcome"}
  57 +end
  58 +
  59 +
38 60 answers[#answers+1] = {"[Leave the mirror alone]"}
39 61
40 62 newChat{ id="welcome",
... ...
  1 +-- ToME - Tales of Maj'Eyal
  2 +-- Copyright (C) 2009 - 2018 Nicolas Casalini
  3 +--
  4 +-- This program is free software: you can redistribute it and/or modify
  5 +-- it under the terms of the GNU General Public License as published by
  6 +-- the Free Software Foundation, either version 3 of the License, or
  7 +-- (at your option) any later version.
  8 +--
  9 +-- This program is distributed in the hope that it will be useful,
  10 +-- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +-- GNU General Public License for more details.
  13 +--
  14 +-- You should have received a copy of the GNU General Public License
  15 +-- along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +--
  17 +-- Nicolas Casalini "DarkGod"
  18 +-- darkgod@te4.org
  19 +
  20 +microtxn = {
  21 + frame_shadow = {x=15, y=15, a=0.5},
  22 + frame_alpha = 1,
  23 + frame_darkness = 0.6,
  24 + frame_ox1 = -60,
  25 + frame_ox2 = 60,
  26 + frame_oy1 = -60,
  27 + frame_oy2 = 60,
  28 +-- title_bar = {x=0, y=-17, w=4, h=25},
  29 +}
\ No newline at end of file
... ...
  1 +-- ToME - Tales of Maj'Eyal
  2 +-- Copyright (C) 2009 - 2018 Nicolas Casalini
  3 +--
  4 +-- This program is free software: you can redistribute it and/or modify
  5 +-- it under the terms of the GNU General Public License as published by
  6 +-- the Free Software Foundation, either version 3 of the License, or
  7 +-- (at your option) any later version.
  8 +--
  9 +-- This program is distributed in the hope that it will be useful,
  10 +-- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +-- GNU General Public License for more details.
  13 +--
  14 +-- You should have received a copy of the GNU General Public License
  15 +-- along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +--
  17 +-- Nicolas Casalini "DarkGod"
  18 +-- darkgod@te4.org
  19 +
  20 +require "engine.class"
  21 +local Dialog = require "engine.ui.Dialog"
  22 +local Textzone = require "engine.ui.Textzone"
  23 +local ActorFrame = require "engine.ui.ActorFrame"
  24 +local List = require "engine.ui.List"
  25 +
  26 +module(..., package.seeall, class.inherit(Dialog))
  27 +
  28 +function _M:init(player, slot)
  29 + self.slot = slot
  30 + self.true_actor = player
  31 + self.actor = player:cloneFull()
  32 + self.actor.x, self.actor.y = nil, nil
  33 + self.actor:removeAllMOs()
  34 +
  35 + Dialog.init(self, "Shimmer: "..self:getShimmerName(), 680, 500)
  36 +
  37 + self:generateList()
  38 +
  39 + self.c_list = List.new{scrollbar=true, width=300, height=self.ih - 5, list=self.list, fct=function(item) self:use(item) end, select=function(item) self:select(item) end}
  40 + local donatortext = ""
  41 + if not profile:isDonator(1) then donatortext = "\n#{italic}##CRIMSON#This cosmetic feature is only available to donators/buyers. You can only preview.#WHITE##{normal}#" end
  42 + local help = Textzone.new{width=math.floor(self.iw - self.c_list.w - 20), height=self.ih, no_color_bleed=true, auto_height=true, text="You can alter your look.\n#{bold}#This is a purely cosmetic change.#{normal}#"..donatortext}
  43 + local actorframe = ActorFrame.new{actor=self.actor, w=128, h=128}
  44 +
  45 + self:loadUI{
  46 + {left=0, top=0, ui=self.c_list},
  47 + {right=0, top=0, ui=help},
  48 + {right=(help.w - actorframe.w) / 2, vcenter=0, ui=actorframe},
  49 + }
  50 + self:setupUI(false, true)
  51 +
  52 + self.key:addBinds{
  53 + EXIT = function()
  54 + game:unregisterDialog(self)
  55 + end,
  56 + }
  57 +end
  58 +
  59 +function _M:getShimmerName()
  60 + if self.slot == "SHIMMER_DOLL" then return "Character's Skin"
  61 + elseif self.slot == "SHIMMER_HAIR" then return "Character's Hair"
  62 + elseif self.slot == "SHIMMER_FACIAL" then return "Character's Facial Features"
  63 + elseif self.slot == "SHIMMER_AURA" then return "Character's Aura"
  64 + end
  65 + return "unknown"
  66 +end
  67 +
  68 +function _M:applyShimmer(actor, shimmer)
  69 + if not shimmer then return self:resetShimmer(actor) end
  70 +
  71 + if self.slot == "SHIMMER_DOLL" then
  72 + actor.moddable_tile_base_shimmer = shimmer.moddable_tile
  73 + elseif self.slot == "SHIMMER_HAIR" then
  74 + actor.moddable_tile_base_shimmer_hair = shimmer.moddable_tile
  75 + elseif self.slot == "SHIMMER_FACIAL" then
  76 + actor.moddable_tile_base_shimmer_facial = shimmer.moddable_tile
  77 + elseif self.slot == "SHIMMER_AURA" then
  78 + actor.moddable_tile_base_shimmer_aura = shimmer.moddable_tile
  79 + actor.moddable_tile_base_shimmer_particle = shimmer.moddable_tile2
  80 + end
  81 +end
  82 +
  83 +function _M:resetShimmer(actor)
  84 + if self.slot == "SHIMMER_DOLL" then
  85 + actor.moddable_tile_base_shimmer = nil
  86 + elseif self.slot == "SHIMMER_HAIR" then
  87 + actor.moddable_tile_base_shimmer_hair = nil
  88 + elseif self.slot == "SHIMMER_FACIAL" then
  89 + actor.moddable_tile_base_shimmer_facial = nil
  90 + elseif self.slot == "SHIMMER_AURA" then
  91 + actor.moddable_tile_base_shimmer_aura = nil
  92 + actor.moddable_tile_base_shimmer_particle = nil
  93 + end
  94 +end
  95 +
  96 +function _M:use(item)
  97 + if not item then end
  98 + game:unregisterDialog(self)
  99 +
  100 + if profile:isDonator(1) then
  101 + self:applyShimmer(self.true_actor, item.moddables)
  102 + self.true_actor:updateModdableTile()
  103 + else
  104 + Dialog:yesnoPopup("Donator Cosmetic Feature", "This cosmetic feature is only available to donators/buyers.", function(ret) if ret then
  105 + game:registerDialog(require("mod.dialogs.Donation").new("shimmer ingame"))
  106 + end end, "Donate", "Cancel")
  107 + end
  108 +end
  109 +
  110 +function _M:select(item)
  111 + if not item then end
  112 + self:applyShimmer(self.actor, item.moddables)
  113 + self.actor:updateModdableTile()
  114 +end
  115 +
  116 +function _M:generateList()
  117 + local unlocked = world.unlocked_shimmers and world.unlocked_shimmers[self.slot] or {}
  118 + local list = {}
  119 +
  120 + list[#list+1] = {
  121 + moddables = nil,
  122 + name = "#GREY#[Default]",
  123 + sortname = "--",
  124 + }
  125 +
  126 + for name, data in pairs(unlocked) do
  127 + local d = {
  128 + moddables = table.clone(data.moddables, true),
  129 + name = name,
  130 + sortname = name:removeColorCodes(),
  131 + }
  132 + d.moddables.name = name
  133 + list[#list+1] = d
  134 + end
  135 + table.sort(list, "sortname")
  136 +
  137 + self.list = list
  138 +end
... ...
... ... @@ -744,6 +744,83 @@ function _M:orderAddonAuthoring(o)
744 744 end
745 745 end
746 746 end
  747 +
  748 +function _M:orderMicroTxn(o)
  749 + if o.suborder == "get_actionables" then
  750 + self:command("MTXN GET_ACTIONABLES", o.module)
  751 + if self:read("200") then
  752 + local _, _, size = self.last_line:find("^([0-9]+)")
  753 + size = tonumber(size)
  754 + local body = {}
  755 + if size and size > 1 then
  756 + body = self:receive(size)
  757 + if body then body = zlib.decompress(body) end
  758 + end
  759 +
  760 + cprofile.pushEvent(("e='MicroTxnListActionables' list=%q"):format(body))
  761 + return
  762 + else
  763 + cprofile.pushEvent(("e='MicroTxnListActionables' error=%q"):format(self.last_error))
  764 + end
  765 + elseif o.suborder == "use_actionable" then
  766 + self:command("MTXN USE_ACTIONABLE", o.module, o.id_purchasable)
  767 +