cjsaylor [Lexaloffle Blog Feed]https://www.lexaloffle.com/bbs/?uid=73269 Catreeboard <p> <table><tr><td> <a href="/bbs/?pid=154463#p"> <img src="/bbs/thumbs/pico8_catreeboard-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=154463#p"> catreeboard</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=154463#p"> [Click to Play]</a> </td></tr></table> </p> <p>A dark storm is looming over the forest. Only you can help our hero cat escape from the rising flood waters that threaten the land.</p> <hr /> <p>Catreeboard is a typing game that helps players practice their typing skills. By successfully typing words on the next branch, the cat will jump in an attempt to avoid rising flood waters.</p> <h3>Keyboard is required to play. Does <strong>not</strong> work with a controller.</h3> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/catreeboard_5.gif" alt="" /> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/catreeboard_2.gif" alt="" /> <h2>Features</h2> <ul> <li>Chill mode for beginning typers to get the basics.</li> <li>Challenge mode to test the fastest of typers, beware the storm!</li> <li>Customize your cat! Select your favorite color for your adventuring cat.</li> <li>Get industry standard metrics on your typing. Each game will report the words per minute (wpm), accuracy, and the number of words typed during the game.</li> </ul> <p><a href="https://www.lexaloffle.com/bbs/?pid=152986#p">Read the devlog</a></p> https://www.lexaloffle.com/bbs/?tid=144297 https://www.lexaloffle.com/bbs/?tid=144297 Wed, 18 Sep 2024 23:40:09 UTC Expanding a dictionary in a word-based game while saving tokens <p>Recently, I've been working on a word-typing game called <a href="https://www.lexaloffle.com/bbs/?tid=143772">Catreeboard</a> with my son. At the beginning and to simply get the game to a state where we could work on animations and gameplay, I naively created a simple table-based dictionary that is subdivided by length of the word. It looked something like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dictionary = { { &quot;a&quot;, &quot;i&quot;, &quot;on&quot;, &quot;at&quot;, &quot;it&quot;, &quot;is&quot;, }, { &quot;cat&quot;, &quot;dog&quot;, &quot;sun&quot;, &quot;hat&quot;, &quot;bat&quot;, &quot;pen&quot;, }, { &quot;home&quot;, &quot;love&quot;, &quot;ball&quot;, &quot;star&quot;, &quot;blue&quot;, }, { &quot;house&quot;, &quot;quick&quot;, &quot;plant&quot;, &quot;water&quot;, &quot;space&quot;, }, { &quot;family&quot;, &quot;person&quot;, &quot;animal&quot;, &quot;window&quot;, &quot;garden&quot;, }, { &quot;picture&quot;, &quot;balance&quot;, &quot;teacher&quot;, &quot;history&quot;, &quot;mountain&quot;, } }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Whenever we wanted a new word, we could get one pretty easily with the following expression:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local word = rnd(dictionary[mid(1, game_level, #dictionary)])</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>&gt; Notice the <code>mid()</code> call here to clamp our selection to what is actually available in the dictionary, this way the game level can continue to grow but not go out of range of our dictionary levels.</p> <p>This was a simple way to get 6 levels worth of words to test with, but now we're at the point where we're ready to expand the dictionary to have more variety. In its current form, every word we added would also be adding a token. We wanted a dictionary of at least 50 words per level with potentially even further levels that would also have 50 variations each. This can add up to a ton of tokens very quickly.</p> <p>Taking a look at the PICO-8 manual to see if there was something we could use to make this more efficient, this passage came to light:</p> <p>&gt; The number of code tokens is shown at the bottom right. One program can have a maximum of 8192 tokens. Each token is a word (e.g. variable name) or operator. Pairs of brackets, and <strong>strings each count as 1 token</strong>. commas, periods, LOCALs, semi-colons, ENDs, and comments are not counted.</p> <p>Combining all the words per level into a single string, we could reduce the tokens to just 6 tokens for all the levels. Our dictionary thus became something like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dictionary = { &quot;a,i,on,at,it&quot;, &quot;cat,dog,sun,hat,bat&quot;, &quot;home,love,ball,star,blue&quot;, &quot;house,quick,plant,water,space&quot;, &quot;family,person,animal,window,garden&quot;, &quot;picture,balance,teacher,history,mountain&quot;, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We now need to do something in order to get a single word. To keep the simplicity of what we're doing to pick a word, we can use a Lua feature called &quot;metatables&quot;. Using metatables, we can intercept a call to an index in the table like <code>dictionary[level]</code> and call a custom function to parse and return the structure that we had before. The first change we need to do is not expose this table with strings directly, so we'll rename <code>dictionary</code> to <code>local packed_dictionary</code>. Next we'll create a metatable function that will read from this and respond with results like before:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local packed_dictionary = { &quot;a,i,on,at,it&quot;, &quot;cat,dog,sun,hat,bat&quot;, &quot;home,love,ball,star,blue&quot;, &quot;house,quick,plant,water,space&quot;, &quot;family,person,animal,window,garden&quot;, &quot;picture,balance,teacher,history,mountain&quot;, } local dictionary_access = { __index = function (table, key) return split(packed_dictionary[key]) end, } dictionary = setmetatable({}, dictionary_access)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Now if I access <code>dictionary[1]</code>, it will give me:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>{ &quot;a&quot;, &quot;i&quot;, &quot;on&quot;, &quot;at&quot;, &quot;it&quot;, &quot;is&quot;, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>However, each time we access <code>dictionary[1]</code> it's going to call our <code>__index</code> function. This is because there is not entry for <code>dictionary</code> at the key of <code>1</code>. Let's use this as an opportunity to &quot;lazy load&quot; and assign it to the dictionary so it doesn't have to constantly parse:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local dictionary_access = { __index = function (table, key) -- table in this instance is `dictionary` table[key] = split(packed_dictionary[key]) return table[key] end, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Finally, we need to be able to get the length of the dictionary in order to be able to do the level selection we were doing before. We can use the <code>__len</code> metatable key to accomplish this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local dictionary_access = { __index = function (table, key) table[key] = split(packed_dictionary[key]) return table[key] end, __len = function (table) return #packed_dictionary end, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>With this in place, we can expand the dictionary of each level without increasing tokens, and any additional &quot;levels&quot; of words we want to add to the dictionary would cost only a single token. We'll just have to keep an eye on the character count.</p> <p>This snippet of code is being used in <a href="https://www.lexaloffle.com/bbs/?tid=143772">Catreeboard</a> currently, however here is a simple cartridge to see it in action without the context of a game around it:</p> <p> <table><tr><td> <a href="/bbs/?pid=153444#p"> <img src="/bbs/thumbs/pico8_zidiyuhuku-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=153444#p"> zidiyuhuku</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=153444#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=143936 https://www.lexaloffle.com/bbs/?tid=143936 Thu, 29 Aug 2024 19:36:06 UTC Catreeboard - A game to help teach typing <p> <table><tr><td> <a href="/bbs/?pid=152986#p"> <img src="/bbs/thumbs/pico8_catreeboard_alpha-3.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=152986#p"> Catreeboard Alpha</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=152986#p"> [Click to Play]</a> </td></tr></table> </p> <p>Catreeboard is a typing game that helps players practice their typing skills. By successfully typing words on the next branch, the cat will jump in an attempt to avoid rising flood waters. I'm making this in collaboration with my son of whom is learning to type.</p> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/catreeboard_5.gif" alt="" /> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/catreeboard_2.gif" alt="" /> <h2>Roadmap</h2> <ul> <li>Main game mode to have levels and calculation of score (based on wpm, accuracy) in addition to infinite scrolling as it is now.</li> <li><del>A losing condition with water rising from the bottom of the screen.</del></li> <li>Sound effects (jumping, game over, etc).</li> <li>Music.</li> <li><del>Better thought out dictionary.</del></li> <li><del>Customized cat color.</del></li> <li><del>Customized difficulty.</del></li> </ul> <h2>Changelog</h2> <p>0.7a:</p> <ul> <li>Expandeded dictionary to over 80 words per word length (6 levels)</li> <li>fix: No back-to-back same words</li> </ul> <p>0.6a:</p> <ul> <li>Implemented challenge mode!</li> <li>Game over screen at the end of challenge mode with metrics displayed.</li> <li>Added menu option to end a game early.</li> <li>Expanded camera capability to scroll in both directions.</li> <li>Improved accuracy from being reduced for hitting non-letter keys.</li> <li>Improved pause behavior.</li> <li>fix: Uneven flood rise.</li> <li>fix: Restore camera after background draw.</li> </ul> https://www.lexaloffle.com/bbs/?tid=143772 https://www.lexaloffle.com/bbs/?tid=143772 Tue, 20 Aug 2024 18:05:43 UTC Matchem' - Traditional match 3 game <p> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_6-6.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_6</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </p> <p>Matchem is a traditional match three gem game, it is designed to be played either with a challenging time/move limited level mode, or a more relaxed endless mode.</p> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/matchem_0.png" alt="" /> <p>Challenge mode:</p> <p>Complete a color objective within a time limit or within a fixed number of moves.</p> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/7_matchem_0.gif" alt="" /> <p>Endless mode:</p> <p>Play forever!</p> <img loading="lazy" style="margin-bottom:16px" border=0 src="/media/73269/matchem_1.gif" alt="" /> <p>Daily Run Mode: Challenge yourself and your friends to achieve the high score of the day! Everyone's board and random gems will be the same each day.</p> <h2>Previous Cart Versions</h2> <h3>0.5</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_5-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_5</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </div></div></div></p> <h3>0.4</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_4-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_4</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </div></div></div></p> <h3>0.3</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_3-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_3</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </div></div></div></p> <h3>0.2</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_2-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_2</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </div></div></div></p> <h3>0.1</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"> <table><tr><td> <a href="/bbs/?pid=123082#p"> <img src="/bbs/thumbs/pico8_matchem_0_1-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=123082#p"> matchem_0_1</a><br><br> by <a href="/bbs/?uid=73269"> cjsaylor</a> <br><br><br> <a href="/bbs/?pid=123082#p"> [Click to Play]</a> </td></tr></table> </div></div></div></p> <h2>Game Notes</h2> <ul> <li>Matching 4 or more gives a &quot;star&quot; gem, which when matched will explode and destroy an entire row and column.</li> <li>Ice gems have to be matched twice to be destroyed.</li> <li>Time gems can add 5 seconds to timed levels in challenge mode when destroyed.</li> </ul> <h2>Roadmap</h2> <ul> <li><del>Animate swapping gems.</del> (0.2)</li> <li><del>Add &quot;combo&quot; displays.</del> (0.3)</li> <li><del>Add scoring.</del> (0.3)</li> <li><del>Add ice gems that require being matched twice to destroy (added difficulty).</del> (0.4)</li> <li><del>With ice gems, cap number of gems required to complete the level and increase ice gem chance instead.</del> (0.4)</li> <li><del>Add move limits to challenge mode.</del> (0.5)</li> <li><del>Add ability to have more than one gem color objective at a time in challenge mode.</del> (Scratched idea)</li> <li><del>Daily run mode: Use current date to seed the randomize function, and go for the highest score of the day.</del> (0.6)</li> <li><del>Additional sfx.</del> (0.6)</li> <li><del>Background music.</del> (0.5)</li> <li>Some different backgrounds (if there is sprite space) or just day and night cycle of current background.</li> </ul> <h2>Changelog</h2> <h3>0.6.5 (2023-01-16)</h3> <ul> <li>Enhanced intro screen. It now uses the board logic to display a mini tutorial on how to play.</li> <li>Update label.</li> </ul> <h3>0.6.4 (2023-01-16)</h3> <ul> <li>Enhanced background animation and imagery.</li> <li>Added option to disable background.</li> </ul> <h3>0.6.3 (2023-01-15)</h3> <p>Bugs:</p> <ul> <li>Fixed issue with rendering result screen.</li> </ul> <h3>0.6.2 (2023-01-15)</h3> <p>Features:</p> <ul> <li>Improved predictability of where a star will be placed. The star will now try to form on the piece you swapped to.</li> <li>Added a hint system that will nudge a piece for a valid move after 3 seconds of inaction. Can be disabled in the menu.</li> </ul> <p>Bugs:</p> <ul> <li>Fixed an issue where the next level would sometimes not get triggered.</li> </ul> <h3>0.6.1 (2023-01-14)</h3> <p>Bugs:</p> <ul> <li>Fixed a crash on the game over results screen after challenge mode.</li> </ul> <h3>0.6 (2023-01-13)</h3> <p>Note, this will likely be the last WIP release prior to going gold.</p> <p>Features:</p> <ul> <li>Daily run mode has been added. This mode is a 2 minute run to see who can get the highest score. The daily score is recorded in cart data so won't be lost when leaving or resetting the cart. The high score will only be reset when the daily run seed changes (next day). See which of your friends is the best Matchem' player!</li> <li>&quot;Time gems&quot; will periodically appear on time based levels in challenge mode that when destroyed will add five seconds to the level clock.</li> <li>Added option to toggle background music in the pause menu.</li> <li>Minor: Display current game version on title screen.</li> </ul> <p>Bug fixes:</p> <ul> <li>Prevent floating text from being right on top of each other.</li> <li>Fixed the weighted gem selection to be closer to expected randomness.</li> <li>Fixed crash in endless mode.</li> </ul> <h3>0.5 (2023-01-08)</h3> <p>Features:</p> <ul> <li>Background music!</li> <li>New level type in challenge mode: Move limits, complete the objective in a fixed number of moves.</li> <li>GUI: Improved challenge mode objectives display.</li> </ul> <p>Bugs:</p> <ul> <li>Fixed player able to move the cursor diagonally.</li> </ul> <h3>0.4 (2022-12-31)</h3> <ul> <li>Added ice blocks which when &quot;destroyed&quot; will simply convert to a normal gem. They do not count towards challenge objective.</li> <li>Capped challenge mode gem objective count to 50, however now each level will increase the odds of ice blocks spawning to replaced destroyed gems.</li> </ul> <h3>0.3 (2022-12-31)</h3> <ul> <li>Added combo indicator.</li> <li>Added scoring.</li> <li>Show floating text of scores when gems are matched.</li> <li>Show highest combo and score on the results screen on completion of challenge mode.</li> </ul> <h3>0.2 (2022-12-28)</h3> <ul> <li>Animate swapping gems.</li> <li>Allow for moving the cursor while animations are occurring so combos don't &quot;feel&quot; frustrating.</li> <li>Draw a cursor state when the player is not allowed to swap (during animations).</li> </ul> <h3>0.1 (2022-12-26)</h3> <ul> <li>Initial WIP release</li> </ul> https://www.lexaloffle.com/bbs/?tid=50852 https://www.lexaloffle.com/bbs/?tid=50852 Mon, 26 Dec 2022 19:18:06 UTC