Yu-Gi-Oh! Deck Building and Card Inventory Management web interface written in Common Lisp, utilizing HTMX.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

352 lignes
15KB

  1. #|
  2. src/web/builder.lisp
  3. "The Deck Builder"
  4. I figure we can also put YDK stuff here since they sorta do the
  5. same thing. Since CSV import comes from Crystal Commerce I've left
  6. that in the crystal-commerce file.
  7. "The old one used to just have a list of cards, and you would
  8. click it, and add it to the deck."
  9. - Yuki, ~July 2023
  10. TODO I think some of these modules are going to have conflicting
  11. names, e.g. RENDER-SAVED-DECK-LIST and SAVED-DECK-LIST ... Maybe
  12. I ought to start packaging this up.
  13. |#
  14. (in-package #:cl-deck-builder2.web)
  15. ;; TODO Not in use yet.
  16. ;; I really should be using https://github.com/40ants/reblocks
  17. (defclass builder-session ()
  18. ((current-deck :accessor builder-session-current-deck
  19. :initform (make-instance 'ydk)
  20. :initarg :deck))
  21. (:documentation "A BUILDER-SESSION encapsulates a deck building session."))
  22. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  23. (defparameter *current-deck* (make-instance 'ydk)
  24. "An instance of YDK that the builder uses as a working region.")
  25. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  26. (defun render-current-deck-list ()
  27. "Helper function. Render the current deck list from *CURRENT-DECK*.
  28. The HTMX JS in the builder is what this gets output to."
  29. (let ((deck-info (ydk-query *current-deck*)))
  30. ;; No Deck info? Just return an empty list.
  31. (render-with-env #P"builder/current-deck-list.html"
  32. `(:active "/builder"
  33. :deck-id ,(ignore-errors (ydk-id-of *current-deck*))
  34. :main-deck ,(ignore-errors (getf deck-info :main-deck))
  35. :extra-deck ,(ignore-errors (getf deck-info :extra-deck))
  36. :side-deck ,(ignore-errors (getf deck-info :side-deck))))))
  37. (defun render-saved-deck-list (&optional id)
  38. "Helper function. Render the saved deck list from SAVED-DECK-LIST."
  39. (render-with-env #P"builder/saved-deck-list.html"
  40. `(:active "/builder"
  41. :current-deck-id ,(ignore-errors
  42. (ydk-id-of *current-deck*))
  43. :id ,id
  44. :saved-deck-list ,(select-ydk-deck))))
  45. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  46. ;; Index route
  47. (defroute ("/builder" :method :GET) (&key _parsed)
  48. "The builder home page. The *CURRENT-DECK* is used as a temporary workspace to create a YDK style deck listing."
  49. (v:info :builder "GET /builder => ~a" _parsed)
  50. (with-logged-in-user
  51. (render-results :active "/builder"
  52. :class 'ygo-info
  53. :params _parsed
  54. :tpl #P"builder/index.html")))
  55. (defroute ("/builder/current-deck-list" :method :GET) ()
  56. "The endpoint for RENDER-CURRENT-DECK-LIST.
  57. TODO This should be re-written as a Redblocks Widget."
  58. ;; Haha, I moved the hack somewhere else. Now it's somebody elses
  59. ;; problem. I actually think I fixed the hack. This comment stays
  60. ;; for now.
  61. (with-logged-in-user
  62. (render-current-deck-list)))
  63. (defroute ("/builder/saved-deck-list" :method :GET) ()
  64. "The endpoint for RENDER-SAVED-DECK-LIST.
  65. TODO This should be re-written as a Redblocks Widget."
  66. (with-logged-in-user
  67. (render-saved-deck-list)))
  68. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  69. ;; Search route
  70. (defroute ("/builder/search" :method :POST) (&key _parsed)
  71. "Main deck builder card search route. Will return a list of data from YGOProDeck. Now souped up and using an ALIST to pass parameters around."
  72. (v:info :builder "POST /builder/search => ~a~%" _parsed)
  73. (with-logged-in-user
  74. (render-results :active "/builder"
  75. :class 'ygo-info
  76. :params _parsed
  77. :tpl #P"builder/search-results.html")))
  78. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  79. ;; POST ROUTES
  80. ;;
  81. ;; TODO Review Logic
  82. ;;
  83. ;; Always Extra, then Side
  84. ;; "fusion"
  85. ;; "link"
  86. ;; "ritual"
  87. ;; "synchro"
  88. ;; "xyz"
  89. ;; "fusion_pendulum"
  90. ;; "ritual_pendulum"
  91. ;; "synchro_pendulum"
  92. ;; "xyz_pendulum"
  93. ;;
  94. ;; TODO Pretty Gnarly!
  95. (defroute ("/builder/add/:passcode" :method :POST) (&key passcode _parsed)
  96. "Try to add PASSCODE to *CURRENT-DECK*.
  97. TODO Parameters: PASSCODE DECK"
  98. (v:info :builder "POST /builder/add/~d => ~a" passcode _parsed)
  99. ;; Clumsily enforce login
  100. (unless (logged-in-p)
  101. (_ "Oops"))
  102. (let ((passcode (ignore-errors
  103. (parse-integer passcode)))
  104. (target-deck (alexandria:make-keyword
  105. (string-upcase
  106. (or
  107. (query-param "deck" _parsed)
  108. "MAIN")))))
  109. (when passcode
  110. (let* ((card-info (ygo-select-info-by-passcode passcode))
  111. (frame-type (ygo-frame-type-of card-info)))
  112. (labels ((is-special (frame-type)
  113. (or
  114. ;; First check what kind of monster it is
  115. (string= "fusion" frame-type)
  116. (string= "link" frame-type)
  117. ;; Ritual cards go in the main deck...
  118. ;; (string= "ritual" frame-type)
  119. (string= "synchro" frame-type)
  120. (string= "xyz" frame-type)
  121. (string= "fusion_pendulum" frame-type)
  122. ;; (string= "ritual_pendulum" frame-type)
  123. (string= "synchro_pendulum" frame-type)
  124. (string= "xyz_pendulum" frame-type)))
  125. (try-add (frame-type passcode target-deck)
  126. (v:info :builder "TRY-ADD ~a ~a ~a" frame-type passcode target-deck)
  127. (cond ((is-special frame-type)
  128. ;; It's a Special Summon - try to add it to the Extra Deck, then Side Deck.
  129. (cond ((< (length (ydk-extra-deck-of *current-deck*)) +extra-deck-card-limit+)
  130. ;; Add to extra deck
  131. (ydk-add *current-deck* :extra passcode))
  132. ((< (length (ydk-side-deck-of *current-deck*)) +extra-deck-card-limit+)
  133. ;; Add to side deck
  134. (ydk-add *current-deck* :side passcode))
  135. (t (v:info :builder (_ "Deck ~a has too many cards~%") (ydk-name-of *current-deck*)))))
  136. ;; So if we get here theoretically the card shouldn't be a "Special Summon"
  137. ((and (eq target-deck :main)
  138. (< (length (ydk-main-deck-of *current-deck*)) +main-deck-card-limit+))
  139. ;; Add to main deck
  140. (ydk-add *current-deck* target-deck passcode))
  141. ((and (eq target-deck :extra)
  142. (< (length (ydk-extra-deck-of *current-deck*)) +extra-deck-card-limit+)
  143. (not (equal frame-type "ritual")))
  144. ;; Add to extra deck
  145. (ydk-add *current-deck* target-deck passcode))
  146. ((and (eq target-deck :side)
  147. (< (length (ydk-side-deck-of *current-deck*)) +extra-deck-card-limit+)
  148. (not (equal frame-type "ritual")))
  149. ;; Add to side deck
  150. (ydk-add *current-deck* target-deck passcode)))))
  151. ;; TODO Where should we validate deck constrains? Here or ADD?
  152. ;; For now we'll do it here.
  153. (if (< (count passcode (ydk-concatenate *current-deck*)) 3)
  154. ;; There *are* fewer than three of this card in the deck...
  155. (try-add frame-type passcode target-deck)
  156. (flash-error (format nil (_ "Deck ~a has too many cards~%") (ydk-name-of *current-deck*))))))
  157. (setf (getf (response-headers *response*) :HX-Trigger) "deck-list-changed")
  158. (render-current-deck-list))))
  159. (defroute ("/builder/remove" :method :POST) (&key _parsed)
  160. "Try to remove PASSCODE from *CURRENT-DECK*.
  161. TODO Parameters: PASSCODE DECK"
  162. (v:info :builder "POST /builder/remove => ~a" _parsed)
  163. (let ((index (query-param "index" _parsed))
  164. (deck (query-param "deck" _parsed)))
  165. (handler-case
  166. (ratify:with-parsed-forms
  167. ((:integer index)
  168. (:string deck))
  169. (ydk-delete-index *current-deck* (alexandria:make-keyword (string-upcase deck)) index)
  170. (setf (getf (response-headers *response*) :HX-Trigger) "deck-list-changed"))
  171. (ratify:combined-error (e)
  172. (flash-error (princ-to-string e))
  173. (redirect "/builder" 302))))
  174. (render-current-deck-list))
  175. (defroute ("/builder/clear" :method :POST) ()
  176. "Clear the *CURRENT-DECK* of any cards using YDK-CLEAR."
  177. (ydk-clear *current-deck*)
  178. (render-current-deck-list))
  179. (defroute ("/builder/create" :method :POST) (&key _parsed)
  180. "Create a new deck. This will preserve some information from *CURRENT-DECK*, giving it a new name, and immediately synchronising it to the database. You may use the CLEAR route to erase the deck."
  181. (v:info :builder "POST /builder/create => ~a" _parsed)
  182. (let ((name (query-param "deck-create-name" _parsed)))
  183. (handler-case
  184. (ratify:with-parsed-forms
  185. ((:string name))
  186. (let ((found (ydk-deck-by-name name)))
  187. (if found
  188. ;; The deck already exists
  189. (progn
  190. (flash-message
  191. (format nil (_ "Deck ~a already exists in database; loading...") name))
  192. (setf *current-deck* (ydk-sync found)))
  193. (progn
  194. (flash-message
  195. (format nil (_ "Creating deck ~a into database.") name))
  196. (setf *current-deck*
  197. (make-instance 'ydk
  198. :name name
  199. :created-by (user-name)
  200. :main-deck (ydk-main-deck-of *current-deck*)
  201. :extra-deck (ydk-extra-deck-of *current-deck*)
  202. :side-deck (ydk-side-deck-of *current-deck*)))
  203. ;; Sync it to the database to save it to the
  204. ;; names list - doesn't matter if it has no
  205. ;; cards in it.
  206. (let ((new (ydk-sync *current-deck*)))
  207. (when new
  208. (setf (ydk-id-of *current-deck*)
  209. (mito:object-id new))))))))
  210. (ratify:combined-error (e)
  211. (flash-error e))))
  212. (render-saved-deck-list))
  213. (defroute ("/builder/delete" :method :DELETE) (&key _parsed)
  214. "Delete the deck specified by DECK-CREATE-NAME in the HTML.
  215. TODO Parameters: NAME The name of the deck to use YDK-DECK-DELETE-BY-NAME on."
  216. (v:info :builder "DELETE /builder/delete => ~a" _parsed)
  217. (let ((name (query-param "deck-create-name" _parsed)))
  218. (handler-case
  219. (ratify:with-parsed-forms
  220. ((:string name))
  221. (ydk-deck-delete-by-name name))
  222. (ratify:combined-error (e)
  223. (flash-error e))))
  224. (render-saved-deck-list))
  225. (defroute ("/builder/save" :method :POST) ()
  226. "Save *CURRENT-DECK* to the database using YDK-SYNC. Return an updated deck listing with RENDER-CURRENT-DECK-LIST."
  227. (v:info :builder "POST /builder/save")
  228. (v:info :builder "~a" (ydk-sync *current-deck*))
  229. (render-current-deck-list))
  230. (defroute ("/builder/load" :method :POST) (&key _parsed)
  231. "Load Deck POST Route. Load ID into *CURRENT-DECK*."
  232. (v:info :builder "POST /builder/load => ~a" _parsed)
  233. (let ((id (query-param "deck-load-id" _parsed)))
  234. (handler-case
  235. (ratify:with-parsed-forms
  236. ((:integer id))
  237. (let ((found (ydk-deck-by-id id)))
  238. (when found
  239. (setf *current-deck* (ydk-sorted (ydk-sync found)))
  240. (flash-message
  241. (format nil (_ "Found entry ~A (~d); loading...~%")
  242. (ydk-name-of found) (mito:object-id found))))))
  243. (ratify:combined-error (e)
  244. (flash-error e))))
  245. (render-current-deck-list))
  246. (defroute ("/builder/rename" :method :POST) (&key _parsed)
  247. "Try to rename a deck. Rename Deck ID to DECK-CREATE-NAME.
  248. TODO There is other code to rename, isn't there? It looks like that code is inteded to be used from the Deck Overview page. This is just in the builder."
  249. (v:info :builder "POST /builder/rename => ~a" _parsed)
  250. (let ((create-name (query-param "deck-create-name" _parsed))
  251. (id (query-param "deck-load-id" _parsed)))
  252. ;; We want to change the name of LOAD-NAME to CREATE-NAME.
  253. (handler-case
  254. (ratify:with-parsed-forms
  255. ((:integer id))
  256. (let ((found (ydk-deck-by-id id)))
  257. (ydk-rename-deck found create-name)
  258. (flash-message
  259. (format nil (_ "Found entry ~A (~d); renaming to ~a.~%")
  260. (ydk-name-of found) id create-name))))
  261. (ratify:combined-error (e)
  262. (flash-error e))))
  263. (render-saved-deck-list))
  264. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  265. ;; Since we're doing this in a builder context I thought it would be
  266. ;; better to do this here than at /category/saved-category-list as
  267. ;; that seemed a bit redundant. Surely there's a better way to
  268. ;; organize all this stuff!
  269. (defroute ("/builder/saved-category-list" :method :GET) (&key _parsed)
  270. "Route for rendering the saved category list.
  271. This is a \"widget\" that we include with Djula, and render separately via HTMX AJAX."
  272. (let ((id (query-param "id" _parsed)))
  273. (handler-case
  274. (ratify:with-parsed-forms
  275. ((:integer id))
  276. (let ((found (find-dao 'category :id id)))
  277. (if found
  278. (render-category found
  279. :tpl #P"builder/saved-category-list.html"))))
  280. (ratify:combined-error (e)
  281. (flash-error e)
  282. (render-category (find-dao 'category :id (max-dao 'category))
  283. :tpl #P"builder/saved-category-list.html")))))
  284. ;; TODO This has got to be better!
  285. (defroute ("/builder/decks-by-cat-id" :method :GET) (&key |id|)
  286. "Saved Deck List helper. Display the saved deck list, and if an |ID| is provided, send that as the current ID."
  287. (with-logged-in-user
  288. (render-with-env #P"builder/saved-deck-list.html"
  289. `(:active "/builder"
  290. :current-deck-id ,(ignore-errors
  291. (ydk-id-of *current-deck*))
  292. :id ,|id|
  293. :saved-deck-list ,(select-ydk-deck
  294. (if |id|
  295. (sxql:where (:= :category-id |id|))))))))
  296. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  297. (defroute ("/builder/move-to-category" :method :POST) (&key _parsed)
  298. "Move *CURRENT-DECK* into CATEGORY ID. TODO Implement this?"
  299. (v:info :builder "GET /builder/move-to-category => ~a" _parsed))