rework crafting checkout page
continuous-integration/drone/push Build is passing Details

This commit is contained in:
MashPotato 2022-07-31 17:54:45 -06:00
parent 3fc26aebc7
commit e2b621e809
4 changed files with 140 additions and 74 deletions

View File

@ -12,6 +12,7 @@ echo Setting env vars
# Set this to the name of the world you play on
export world="excalibur"
export datacenter="primal"
# Define the names of your retainers - this is used for the "checkmark" that's displayed
# next to items that you already have listed on the market to help you avoid posting duplicate items

View File

@ -83,7 +83,13 @@ func staleItemsPageHandler(w http.ResponseWriter, _ *http.Request) {
// When GET, produces a list of the items to crafting, suggested quantities to craft, and materials required
func craftingCheckoutPageHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
type TableItem struct {
type Retainer struct {
Name string
ItemsToGet []inventorytools.Item
}
type CraftingItem struct {
ID int
Name string
Job string
@ -91,16 +97,18 @@ func craftingCheckoutPageHandler(w http.ResponseWriter, r *http.Request) {
}
type PageData struct {
Title string
CraftingItems []TableItem // The items that will be crafted
CraftingMaterials []TableItem // The materials required to craft
Title string
CraftingItems []CraftingItem // The items that will be crafted
Retainers []Retainer // items to get from retainers
MarketItems []inventorytools.Item // items that need to be bought from market
}
pageData := PageData{Title: "Crafting Checkout", CraftingItems: make([]TableItem, 0), CraftingMaterials: make([]TableItem, 0)}
craftingMaterialMap := make(map[int]int)
pageData := PageData{Title: "Crafting Checkout", CraftingItems: make([]CraftingItem, 0), Retainers: make([]Retainer, 0), MarketItems: make([]inventorytools.Item, 0)}
// Build list of crafting items, and how much materials are required
craftingMaterialMap := make(map[int]int) // key item ID, value quantity required
for _, craftingItemID := range craftingCheckoutItemIDs {
craftingItem := TableItem{ID: craftingItemID, Name: items[craftingItemID].Name, Job: items[craftingItemID].Job, Quantity: (items[craftingItemID].Velocity / 2) + 1}
craftingItem := CraftingItem{ID: craftingItemID, Name: items[craftingItemID].Name, Job: items[craftingItemID].Job, Quantity: (items[craftingItemID].Velocity / 2) + 1}
for _, materialItem := range items[craftingItemID].CraftingMaterials {
if _, ok := craftingMaterialMap[materialItem.ID]; ok {
// we've seen it before, so we add to it
@ -113,11 +121,57 @@ func craftingCheckoutPageHandler(w http.ResponseWriter, r *http.Request) {
pageData.CraftingItems = append(pageData.CraftingItems, craftingItem)
}
for id, quantity := range craftingMaterialMap {
materialItem := TableItem{ID: id, Name: items[id].Name, Quantity: quantity}
pageData.CraftingMaterials = append(pageData.CraftingMaterials, materialItem)
// Subtract materials in character inventory
for _, item := range inventories.CharacterBags {
if _, ok := craftingMaterialMap[item.ID]; ok {
craftingMaterialMap[item.ID] = craftingMaterialMap[item.ID] - item.Quantity
if craftingMaterialMap[item.ID] <= 0 {
delete(craftingMaterialMap, item.ID)
}
}
}
// Materials in retainer inventory and subtract
for _, retainer := range inventories.Retainers {
itemsToGet := make([]inventorytools.Item, 0)
for _, item := range retainer.RetainerBags {
if _, ok := craftingMaterialMap[item.ID]; ok {
// This is an item we need that exists on the retainer
if item.Quantity > craftingMaterialMap[item.ID] { // the retainer has more than we need, so adjust quantity down to what we need
item.Quantity = craftingMaterialMap[item.ID]
}
// offset quantity and map name from ID
itemToGet := item
itemToGet.Name = items[itemToGet.ID].Name
itemToGet.SortedContainer -= 9999
itemToGet.SortedSlotIndex += 1
itemsToGet = append(itemsToGet, itemToGet)
craftingMaterialMap[item.ID] = craftingMaterialMap[item.ID] - item.Quantity
if craftingMaterialMap[item.ID] <= 0 {
delete(craftingMaterialMap, item.ID)
}
}
}
if len(itemsToGet) > 0 {
pageData.Retainers = append(pageData.Retainers, Retainer{Name: retainer.Name, ItemsToGet: itemsToGet})
}
}
// Materials to be bought from market is what's left over
for itemID, itemQuantity := range craftingMaterialMap {
itemToGet := inventorytools.Item{ID: itemID, Name: items[itemID].Name, Quantity: itemQuantity}
pageData.MarketItems = append(pageData.MarketItems, itemToGet)
}
/*
for id, quantity := range craftingMaterialMap {
materialItem := TableItem{ID: id, Name: items[id].Name, Quantity: quantity}
pageData.CraftingMaterials = append(pageData.CraftingMaterials, materialItem)
}
*/
err := t.ExecuteTemplate(w, "crafting-checkout.html", pageData)
if err != nil {
log.Printf("failed to execute template: %v", err)
@ -147,6 +201,27 @@ func craftingCheckoutPageHandler(w http.ResponseWriter, r *http.Request) {
craftingCheckoutItemIDs = make([]int, 0)
craftingCheckoutItemIDs = craftingCheckoutPageData.ItemIDs
go func() { // queue up datacenter price updates
universalisPriceEndpoint = datacenter
defer func() { universalisPriceEndpoint = world }() // set back to world when done
// build list of crafting supply item IDs to be refreshed for datacenter pricing
itemsToRefresh := make(map[int]bool)
for _, craftingItemID := range craftingCheckoutItemIDs {
for _, craftingMaterial := range items[craftingItemID].CraftingMaterials {
itemsToRefresh[craftingMaterial.ID] = true
}
}
// queue up datacenter price updates
for itemID := range itemsToRefresh {
priceUpdateQueue <- itemID
updateRequestWg.Add(1)
}
// wait for channel to empty
updateRequestWg.Wait()
}()
}
}

36
main.go
View File

@ -19,21 +19,22 @@ import (
)
var (
items map[int]*MarketItem // hash map used to quickly guarantee uniqueness of MarketItems
itemSlice []MarketItem // rank-ordered slice of the same data in the items map
nameUpdateQueue chan int // requests sent to this channel use xivapi https://xivapi.com/
priceUpdateQueue chan int // requests sent to this channel use universalis.app API https://universalis.app/docs/index.html
updateRequestWg sync.WaitGroup // Used to wait until the queues are empty indicating all items have up to date names and prices
t *template.Template // Used to cache HTML template parsing, done once at startup
lastFullPriceUpdateTime time.Time // The last time the prices were updated from Universalis (both start and finish. Start if ongoing, finished when not running)
startFullPriceUpdateTime time.Time // The last time the prices were started to be updated
lastSortTime time.Time // The last time the itemSlice was sorted by metric
lastUniversalisRequestTime time.Time // The last time any request was made to Universalis
priceUpdateProgress int // an integer from 0-100 indicating percentage of any ongoing Universalis update. If 100, then the update is complete.
world string // filter Universalis to this world name
retainers []string // contains name of retainers
craftingCheckoutItemIDs []int // The item IDs which were selected on the crafting checkout page, used for shopping list
inventories inventorytools.Inventories
items map[int]*MarketItem // hash map used to quickly guarantee uniqueness of MarketItems
itemSlice []MarketItem // rank-ordered slice of the same data in the items map
nameUpdateQueue chan int // requests sent to this channel use xivapi https://xivapi.com/
priceUpdateQueue chan int // requests sent to this channel use universalis.app API https://universalis.app/docs/index.html
updateRequestWg sync.WaitGroup // Used to wait until the queues are empty indicating all items have up to date names and prices
t *template.Template // Used to cache HTML template parsing, done once at startup
lastFullPriceUpdateTime time.Time // The last time the prices were updated from Universalis (both start and finish. Start if ongoing, finished when not running)
startFullPriceUpdateTime time.Time // The last time the prices were started to be updated
lastSortTime time.Time // The last time the itemSlice was sorted by metric
lastUniversalisRequestTime time.Time // The last time any request was made to Universalis
priceUpdateProgress int // an integer from 0-100 indicating percentage of any ongoing Universalis update. If 100, then the update is complete.
world, datacenter, universalisPriceEndpoint string // filter Universalis to this world name
retainers []string // contains name of retainers
craftingCheckoutItemIDs []int // The item IDs which were selected on the crafting checkout page, used for shopping list
inventories inventorytools.Inventories
)
// Object to represent a crafting material / ingredient in a recipe
@ -142,6 +143,7 @@ func init() {
// Validate that all required environment variables are set
envVars := make(map[string]string)
envVars["world"] = os.Getenv("world")
envVars["datacenter"] = os.Getenv("datacenter")
for key, value := range envVars {
if value == "" {
log.Fatalf("required environment variable '%s' is not set or has empty value", key)
@ -158,6 +160,8 @@ func init() {
}
world = envVars["world"]
datacenter = envVars["datacenter"]
universalisPriceEndpoint = world
updateRequestWg = sync.WaitGroup{}
nameUpdateQueue = make(chan int, 8192)
@ -282,7 +286,7 @@ func priceUpdateWorker() {
// By this point we know the item name is not found in the cache
// and this is the first request to populate it
resp, err := http.Get(fmt.Sprintf("https://universalis.app/api/%s/%d", world, id))
resp, err := http.Get(fmt.Sprintf("https://universalis.app/api/%s/%d", universalisPriceEndpoint, id))
if err != nil {
log.Printf("request to get item ID '%d' price failed: %v", id, err)
return

View File

@ -9,7 +9,8 @@
name: "{{ .Name }}",
})
{{ end }}
{{ range .CraftingMaterials }}
{{ range .MarketItems }}
itemList.push({
id: {{ .ID }},
name: "{{ .Name }}",
@ -21,41 +22,6 @@
console.log("setSelected state: " + stateID)
$('#state-'+selectedItem).html(stateID)
}
document.onkeydown = checkKeycode
function checkKeycode(e) {
var keycode;
if (window.event)
{keycode = window.event.keyCode;}
else if (e)
{keycode = e.which;}
if (selectedItem == 0) { // there is no item selected
return
}
console.log("keypressed: "+ keycode)
if (keycode == 48) { // 0
setSelected(0)
} else if (keycode == 49) { //1
setSelected(1)
} else if (keycode == 50) { //2
setSelected(2)
} else if (keycode == 51) { //3
setSelected(3)
} else if (keycode == 52) { //4
setSelected(4)
} else if (keycode == 53) { //5
setSelected(5)
} else if (keycode == 54) { //6
setSelected(6)
} else if (keycode == 55) { //7
setSelected(7)
} else if (keycode == 56) { //8
setSelected(8)
} else if (keycode == 57) { //9
setSelected(9)
}
}
})
// Sets the clipboard to the item name and highlights the item row
@ -77,7 +43,7 @@
<p>This page lists the items that were selected for crafting before checkout, along with the shopping list of materials required for the craft</p>
</div>
<h3 class="ui header">Items to Craft</h3>
<h2 class="ui header">Items to Craft</h3>
<table id="itemTable" class="ui compact celled striped table">
<thead>
<tr>
@ -101,25 +67,45 @@
</tbody>
</table>
<h3 class="ui header">Materials Required</h3>
<table id="craftingMaterialTable" class="ui compact celled striped table">
<h2 class="ui header">Materials From Retainers</h2>
{{ range .Retainers }}
<h3 class="ui header">{{ .Name }}</h4>
<table class="ui compact celled striped table">
<thead>
<tr>
<th>Item Name</th>
<th>State</th>
<th>Container</th>
<th>Slot</th>
<th>Quantity</th>
</tr>
</thead>
<tbody id="craftingMaterialTableBody">
{{ range .CraftingMaterials }}
{{ range .ItemsToGet }}
<tr>
<td><h4>{{ .Name }}</h4></td>
<td>{{ .SortedContainer }}</td>
<td>{{ .SortedSlotIndex }}</td>
<td>{{ .Quantity }}</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}
<h2 class="ui header">Materials From Market</h2>
<table class="ui compact celled striped table">
<thead>
<tr>
<th>Item Name</th>
<th>Quantity</th>
</tr>
</thead>
<tbody id="craftingMaterialTableBody">
{{ range .MarketItems }}
<tr id="tr-{{ .ID }}">
<td data-label="Item Name">
<h4><button class="ui circular icon button" onclick="selectItem({{ .ID }})"><i class="icon copy outline"></i></button>{{ .Name }}</h4>
</td>
<td class="center aligned" data-label="State">
<div id="state-{{ .ID }}"></div>
</td>
<td class="right aligned" data-label="Quanity">{{ .Quantity }}</td>
<td><h4><button class="ui circular icon button" onclick="selectItem({{ .ID }})"><i class="icon copy outline"></i></button>{{ .Name }}</h4></td>
<td>{{ .Quantity }}</td>
</tr>
{{ end }}
</tbody>