Skip to main content
GameDev.net gamedev.net
🔒 Locked

This is just wrong.

Started by Michalson Oct 30, 2002 at 9:11 AM 50 replies 750+ views
Original Post
Michalson
Michalson
Link The controls are:
       /\
<- J  K  L  ->
       M
       \/

Shoot: Space
 
The page is under 5K and uses only &#106avascript (self contained).
Katta
Katta
You bet it''s wrong. I can''t even straf!

That is so cool man, did you see the actual sitehere?
[Slime] I saw a headline on the newspaper today: "Horrific Rape in Alley", or something.[Slime] I was like "Is there any other kind of rape?"[Slime] "HILARIOUS RAPE AS CLOWN SODOMIZED"
Alimonster
Alimonster
Similar stuff:
Wolfen3d (4K exe file)
Wolf (256 bytes).

They''re not complete games, however IIRC. If you''re interested, here''s some 4K Descent-style thingy (with MIDI music if you can set it up under DOS).
Michalson
Michalson
quote:
Original post by Alimonster
Similar stuff:
Wolfen3d (4K exe file)
Wolf (256 bytes ).

They're not complete games, however IIRC. If you're interested, here's some 4K Descent-style thingy (with MIDI music if you can set it up under DOS).


While nice, those are all executables. This is an HTML page with a few <script> tags thrown in. It's like making Pong in Excel (actually I could probably...)

EDIT: Never mind, someone already has:



[edited by - michalson on October 30, 2002 11:02:48 AM]
masterg
masterg
Michalson: Very interesting link. I''d really like to see pong done in Excel, if you have the time.
masterghttp:/masterg.andyc.org
Katta
Katta
Pong in Excel, very interesting... and fun...
[Slime] I saw a headline on the newspaper today: "Horrific Rape in Alley", or something.[Slime] I was like "Is there any other kind of rape?"[Slime] "HILARIOUS RAPE AS CLOWN SODOMIZED"
GameCreator
GameCreator
Incredible. Some people are just born coders.
(Then there''s me. )
bishop_pass
bishop_pass
I don''t quite understand the motivation to prove that you can do something like that. I would much rather expend my energy coding something cutting edge, rather than rehashing what has been done before, but in a lesser environment.
_______________________________
"To understand the horse you'll find that you're going to be working on yourself. The horse will give you the answers and he will question you to see if you are sure or not."
- Ray Hunt, in
Stoffel
Stoffel
OMFG smiley campers fragged me.
Facehat
Facehat
WTF!! OMG LAG!!!!

----------------------------------------
"Before criticizing someone, walk a mile in their shoes.
Then, when you do criticize them, you will be a mile away and have their shoes." -- Deep Thoughts
"If you have any trouble sounding condescending, find a Unix user to show you how it''s done." - Scott Adams
FaceHat Software -- Wear the hat.
benjamin bunny
benjamin bunny
quote:
Original post by bishop_pass
I don't quite understand the motivation to prove that you can do something like that. I would much rather expend my energy coding something cutting edge, rather than rehashing what has been done before, but in a lesser environment.


It's _supposed_ to be pointless. The pointlessness only makes it more impressive .

____________________________________________________________
www.elf-stone.com

[edited by - benjamin bunny on October 30, 2002 6:04:52 PM]
Sneftel
Sneftel
quote:
Original post by bishop_pass
I don''t quite understand the motivation to prove that you can do something like that. I would much rather expend my energy coding something cutting edge, rather than rehashing what has been done before, but in a lesser environment.


Well, it''s all the thrill of increasing the efficiency of resource use. One way to do it is to do more stuff with the same resources; the other way is to do the same stuff with less resources.

As for whether it would ever actually be useful.... I imagine people porting games to the GBA would find it a VERY helpful skill.


Don''t listen to me. I''ve had too much coffee.
bishop_pass
bishop_pass
Pointless code doesn''t impress me that much, unless it exhibits some new and interesting behavior.
_______________________________
"To understand the horse you'll find that you're going to be working on yourself. The horse will give you the answers and he will question you to see if you are sure or not."
- Ray Hunt, in
Cedric
Cedric
It''s a pastime. Not everyone is coding to impress others, or to achieve something. Why are you interested in horses? I''m sure you have plenty of good reasons, but being interested in horses doesn''t accomplish much in itself (at least, not much more than coding pointless stuff).

Cédric
bishop_pass
bishop_pass
Hey, I'm interested in coding too. But when I do it, I'm not interested in making a lesser version of something already widely available.

Whatever. To each their own. Suit yourself. I'd rather spend my time coding ray tracing code, or neural networks, or vertex min cuts, or fractal plant growth.

The program referenced here is a visual program. And it looks like shit. Why doesn't the guy spend his time (and talent) doing some cutting edge 3d that looks good. Exploit the best there is, not the minimal common denominator.

You want to see some impressive Java realtime 3d code? Go to Ken Perlin's website.

[edited by - bishop_pass on October 31, 2002 1:05:46 AM]
_______________________________
"To understand the horse you'll find that you're going to be working on yourself. The horse will give you the answers and he will question you to see if you are sure or not."
- Ray Hunt, in
KalvinB
KalvinB
If you can make a wolfenstien clone run in HTML/&#106avascript imagine what you could do with a real language.<br><br>The art of doing a lot with a little (back in the days of the 286 we were forced to) is getting lost more and more.<br><br>Back when Wolfenstien 3D first came out it was thought to be some incredible feat. Then people got a look at the source and it turned out people were just thinking too hard about the problem.<br><br>Now we''re finding out that Wolfenstein could have been coded well before it actually was.<br><br>As a demo it''s not too impressive. It''s the ability to take such a seemingly complex idea and cut down to the size of Pouya''s Tetris that''s quite impressive.<br><br>Ben <br><br><font size="-2"><br><b><a href="http://www.icarusindie.com">IcarusIndie.com</a></b> [ <a href="http://www.icarusindie.com">The Rabbit Hole</a> | <a href="http://www.icarusindie.com/labyrinth/">The Labyrinth</a> | <a href="http://devzone.icarusindie.com">DevZone</a> | <a href="http://www.icarusindie.com/Gang Wars/">Gang Wars</a> | <a href="http://www.icarusindie.com/cgi-bin/thewall/thewall.cgi">The Wall</a> | <a href="http://www.icarusindie.com/hosting/">Hosting</a> | <a href="http://www.cafeshops.com/cp/store.aspx?s=icarusindie">Tiberian Merchandise!!!!</a> | <a href="http://www.icarusindie.com/rate/">GameShot</a> ]</font> </div> <footer class="mt-auto pt-3 border-t border-gray-100 dark:border-gray-700"> <div class="mb-3 forum-signature text-gray-400 dark:text-gray-500"><span>[ <a href="http://www.icarusindie.com">IcarusIndie.com</a> | <a href="http://www.recycledrussianbrides.com">recycledrussianbrides.com</a> | <a href="http://www.icarusindie.com/distribution/">Got Linux?</a> ]</span><br> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&amp;business=kalvinb602@hotmail.com&amp;item_name=Feed%20Me&amp;item_number=9000&amp;amount=1.00&amp;no_note=1¤cy_code=USD&amp;tax=0">&lt;img src='https://www.paypal.com/images/x-click-but21.gif' alt</a></div> <div class="flex items-center justify-between flex-wrap gap-2 text-xs"> <div class="hidden sm:flex items-center gap-2 text-gray-400 dark:text-gray-500"> <time title="October 31, 2002 12:07 AM">Oct 31, 2002 at 12:07 AM</time> </div> <div class="flex items-center gap-1 sm:gap-2 flex-wrap text-xs"> <div class="reactions-group flex items-center gap-1" id="reactions-1664725"> <button type="button" hx-post="/forums/post/1664725/react/" hx-vals='{"type": "like"}' hx-target="#reactions-1664725" hx-swap="outerHTML" title="Like this post" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-rose-500 dark:hover:text-rose-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/> </svg> <span class="hidden sm:inline">Like</span> </button> <button type="button" hx-post="/forums/post/1664725/react/" hx-vals='{"type": "helpful"}' hx-target="#reactions-1664725" hx-swap="outerHTML" title="This is helpful" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-emerald-500 dark:hover:text-emerald-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> </svg> <span class="hidden sm:inline">Helpful</span> </button> <button type="button" hx-post="/forums/post/1664725/react/" hx-vals='{"type": "thanks"}' hx-target="#reactions-1664725" hx-swap="outerHTML" title="Thanks for this" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-amber-500 dark:hover:text-amber-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/> </svg> <span class="hidden sm:inline">Thanks</span> </button> </div> <span class="hidden sm:inline text-gray-200 dark:text-gray-700 mx-1">|</span> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="quotePost(1664725, 'KalvinB')" title="Quote this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg> <span class="hidden sm:inline">Quote</span> </button> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="copyPostUrl(1664725)" title="Copy link to this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <span class="hidden sm:inline">Link</span> </button> </div> </div> </footer> </div> </div> </div> </article> <article id="post-1664726" data-author-name="HunterTKilla" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col min-h-[200px]"> <div class="p-6 flex-1 flex flex-col"> <div class="flex gap-4 flex-1"> <aside class="hidden sm:block w-28 flex-shrink-0 text-center"> <a href="/profile/29847-huntertkilla" class="inline-block"> <span class="gdx-avatar w-12 h-12 rounded-full "> <span class="gdx-avatar__img gdx-avatar__fallback rounded-full cursor-pointer text-lg" data-user-tooltip data-member-id="29847"> <span aria-hidden="true">H</span> </span> </span> </a> <a href="/profile/29847-huntertkilla" class="mt-2 block text-sm font-medium text-gray-900 dark:text-white hover:text-sky-600 truncate" data-tooltip-target="tooltip-author-29847"> HunterTKilla </a> <div class="flex flex-wrap justify-center gap-1 mt-1"> </div> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-center gap-1"> <svg class="w-3 h-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg> 24 </p> </aside> <div class="flex-1 min-w-0 flex flex-col"> <div class="sm:hidden mb-4"> <div class="flex items-center gap-1.5"> <span class="gdx-avatar w-6 h-6 rounded-full ring-2 ring-white dark:ring-gray-800"> <img src="/static/media/user/generic-user.png" alt="HunterTKilla" class="gdx-avatar__img rounded-full cursor-pointer object-cover" data-user-tooltip data-member-id="29847"> </span> <div> <a href="/profile/29847-huntertkilla" class="font-medium text-gray-900 dark:text-white hover:text-purple-600 text-xs" data-user-tooltip data-member-id="29847"> HunterTKilla </a> <div class="text-[10px] text-gray-500 dark:text-gray-400"> <time>Oct 31, 2002 at 12:07 AM</time> </div> </div> </div> </div> <div id="post-content-1664726" class="prose dark:prose-invert max-w-none flex-1 pb-3"> I made a pong game with AI in excel &#111;nce cause all I had was my crappy 233 mHz laptop with me, thats probably hy ppl do it <img src="tongue.gif" width=15 height=15 align=middle> </div> <footer class="mt-auto pt-3 border-t border-gray-100 dark:border-gray-700"> <div class="mb-3 forum-signature text-gray-400 dark:text-gray-500"><a href="http://www.standonguard.com"></a></div> <div class="flex items-center justify-between flex-wrap gap-2 text-xs"> <div class="hidden sm:flex items-center gap-2 text-gray-400 dark:text-gray-500"> <time title="October 31, 2002 12:07 AM">Oct 31, 2002 at 12:07 AM</time> </div> <div class="flex items-center gap-1 sm:gap-2 flex-wrap text-xs"> <div class="reactions-group flex items-center gap-1" id="reactions-1664726"> <button type="button" hx-post="/forums/post/1664726/react/" hx-vals='{"type": "like"}' hx-target="#reactions-1664726" hx-swap="outerHTML" title="Like this post" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-rose-500 dark:hover:text-rose-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/> </svg> <span class="hidden sm:inline">Like</span> </button> <button type="button" hx-post="/forums/post/1664726/react/" hx-vals='{"type": "helpful"}' hx-target="#reactions-1664726" hx-swap="outerHTML" title="This is helpful" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-emerald-500 dark:hover:text-emerald-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> </svg> <span class="hidden sm:inline">Helpful</span> </button> <button type="button" hx-post="/forums/post/1664726/react/" hx-vals='{"type": "thanks"}' hx-target="#reactions-1664726" hx-swap="outerHTML" title="Thanks for this" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-amber-500 dark:hover:text-amber-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/> </svg> <span class="hidden sm:inline">Thanks</span> </button> </div> <span class="hidden sm:inline text-gray-200 dark:text-gray-700 mx-1">|</span> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="quotePost(1664726, 'HunterTKilla')" title="Quote this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg> <span class="hidden sm:inline">Quote</span> </button> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="copyPostUrl(1664726)" title="Copy link to this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <span class="hidden sm:inline">Link</span> </button> </div> </div> </footer> </div> </div> </div> </article> <article id="post-1664833" data-author-name="Taulin" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col min-h-[200px]"> <div class="p-6 flex-1 flex flex-col"> <div class="flex gap-4 flex-1"> <aside class="hidden sm:block w-28 flex-shrink-0 text-center"> <a href="/profile/4637-taulin" class="inline-block"> <span class="gdx-avatar w-12 h-12 rounded-full "> <span class="gdx-avatar__img gdx-avatar__fallback rounded-full cursor-pointer text-lg" data-user-tooltip data-member-id="4637"> <span aria-hidden="true">T</span> </span> </span> </a> <a href="/profile/4637-taulin" class="mt-2 block text-sm font-medium text-gray-900 dark:text-white hover:text-sky-600 truncate" data-tooltip-target="tooltip-author-4637"> Taulin </a> <div class="flex flex-wrap justify-center gap-1 mt-1"> </div> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-center gap-1"> <svg class="w-3 h-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg> 73 </p> </aside> <div class="flex-1 min-w-0 flex flex-col"> <div class="sm:hidden mb-4"> <div class="flex items-center gap-1.5"> <span class="gdx-avatar w-6 h-6 rounded-full ring-2 ring-white dark:ring-gray-800"> <img src="/static/media/user/generic-user.png" alt="Taulin" class="gdx-avatar__img rounded-full cursor-pointer object-cover" data-user-tooltip data-member-id="4637"> </span> <div> <a href="/profile/4637-taulin" class="font-medium text-gray-900 dark:text-white hover:text-purple-600 text-xs" data-user-tooltip data-member-id="4637"> Taulin </a> <div class="text-[10px] text-gray-500 dark:text-gray-400"> <time>Oct 31, 2002 at 3:48 AM</time> </div> </div> </div> </div> <div id="post-content-1664833" class="prose dark:prose-invert max-w-none flex-1 pb-3"> <BLOCKQUOTE><SPAN CLASS=smallfont>quote:<hr HEIGHT=1 noshade><i>Original post by bishop_pass </i> <br>Pointless code doesn''t impress me that much, unless it exhibits some new and interesting behavior. <hr height=1 noshade></SPAN></BLOCKQUOTE> <br><br>Your single, right?<br> </div> <footer class="mt-auto pt-3 border-t border-gray-100 dark:border-gray-700"> <div class="mb-3 forum-signature text-gray-400 dark:text-gray-500"><a href="http://www.interneko.com"></a></div> <div class="flex items-center justify-between flex-wrap gap-2 text-xs"> <div class="hidden sm:flex items-center gap-2 text-gray-400 dark:text-gray-500"> <time title="October 31, 2002 03:48 AM">Oct 31, 2002 at 3:48 AM</time> </div> <div class="flex items-center gap-1 sm:gap-2 flex-wrap text-xs"> <div class="reactions-group flex items-center gap-1" id="reactions-1664833"> <button type="button" hx-post="/forums/post/1664833/react/" hx-vals='{"type": "like"}' hx-target="#reactions-1664833" hx-swap="outerHTML" title="Like this post" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-rose-500 dark:hover:text-rose-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/> </svg> <span class="hidden sm:inline">Like</span> </button> <button type="button" hx-post="/forums/post/1664833/react/" hx-vals='{"type": "helpful"}' hx-target="#reactions-1664833" hx-swap="outerHTML" title="This is helpful" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-emerald-500 dark:hover:text-emerald-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> </svg> <span class="hidden sm:inline">Helpful</span> </button> <button type="button" hx-post="/forums/post/1664833/react/" hx-vals='{"type": "thanks"}' hx-target="#reactions-1664833" hx-swap="outerHTML" title="Thanks for this" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-amber-500 dark:hover:text-amber-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/> </svg> <span class="hidden sm:inline">Thanks</span> </button> </div> <span class="hidden sm:inline text-gray-200 dark:text-gray-700 mx-1">|</span> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="quotePost(1664833, 'Taulin')" title="Quote this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg> <span class="hidden sm:inline">Quote</span> </button> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="copyPostUrl(1664833)" title="Copy link to this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <span class="hidden sm:inline">Link</span> </button> </div> </div> </footer> </div> </div> </div> </article> <article id="post-1664839" data-author-name="bishop_pass" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col min-h-[200px]"> <div class="p-6 flex-1 flex flex-col"> <div class="flex gap-4 flex-1"> <aside class="hidden sm:block w-28 flex-shrink-0 text-center"> <a href="/profile/5682-bishop_pass" class="inline-block"> <span class="gdx-avatar w-12 h-12 rounded-full "> <span class="gdx-avatar__img gdx-avatar__fallback rounded-full cursor-pointer text-lg" data-user-tooltip data-member-id="5682"> <span aria-hidden="true">B</span> </span> </span> </a> <a href="/profile/5682-bishop_pass" class="mt-2 block text-sm font-medium text-gray-900 dark:text-white hover:text-sky-600 truncate" data-tooltip-target="tooltip-author-5682"> bishop_pass </a> <div class="flex flex-wrap justify-center gap-1 mt-1"> </div> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-center gap-1"> <svg class="w-3 h-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg> 297 </p> </aside> <div class="flex-1 min-w-0 flex flex-col"> <div class="sm:hidden mb-4"> <div class="flex items-center gap-1.5"> <span class="gdx-avatar w-6 h-6 rounded-full ring-2 ring-white dark:ring-gray-800"> <img src="/static/media/user/generic-user.png" alt="bishop_pass" class="gdx-avatar__img rounded-full cursor-pointer object-cover" data-user-tooltip data-member-id="5682"> </span> <div> <a href="/profile/5682-bishop_pass" class="font-medium text-gray-900 dark:text-white hover:text-purple-600 text-xs" data-user-tooltip data-member-id="5682"> bishop_pass </a> <div class="text-[10px] text-gray-500 dark:text-gray-400"> <time>Oct 31, 2002 at 4:05 AM</time> </div> </div> </div> </div> <div id="post-content-1664839" class="prose dark:prose-invert max-w-none flex-1 pb-3"> Well, KalvinB, thank you for your explanation. Bear in mind that my initial foray into computer programming was by hand assembling 6502 assembly into machine code &#111;n an Apple II that had 6 colors and 48k of memory (16k of which was display memory). I worked with clever coding tricks like ''hyperscroll'' which was inlined line by line scrolling code.<br><br>Back in ''83, I wrote a program that precomputed wireframe vertex info and then I hardcoded this info into assembly for a realtime playback of 3d animation.<br><br>To your admiration of stuff like this, I say, to each your own, I''ve been there and done that, and I''ll take all the machine resources I can get to build something more interesting.<br><br>I''ve worked with the unruly HAM mode &#111;n the Amiga, using error diffusion to correct for color banding with a ray tracer that I wrote (back in 1987 when ray tracers were just appearing &#111;n home computers) and were mostly being written &#111;n expensive workstations and VAX 11/780s. </div> <footer class="mt-auto pt-3 border-t border-gray-100 dark:border-gray-700"> <div class="mb-3 forum-signature text-gray-400 dark:text-gray-500"> _______________________________<br><span>"To understand the horse you'll find that you're going to be working on yourself. The horse will give you the answers and he will question you to see if you are sure or not."</span><br> - <a href="http://www.rayhunt.com" rel="external">Ray Hunt</a>, in </div> <div class="flex items-center justify-between flex-wrap gap-2 text-xs"> <div class="hidden sm:flex items-center gap-2 text-gray-400 dark:text-gray-500"> <time title="October 31, 2002 04:05 AM">Oct 31, 2002 at 4:05 AM</time> </div> <div class="flex items-center gap-1 sm:gap-2 flex-wrap text-xs"> <div class="reactions-group flex items-center gap-1" id="reactions-1664839"> <button type="button" hx-post="/forums/post/1664839/react/" hx-vals='{"type": "like"}' hx-target="#reactions-1664839" hx-swap="outerHTML" title="Like this post" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-rose-500 dark:hover:text-rose-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/> </svg> <span class="hidden sm:inline">Like</span> </button> <button type="button" hx-post="/forums/post/1664839/react/" hx-vals='{"type": "helpful"}' hx-target="#reactions-1664839" hx-swap="outerHTML" title="This is helpful" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-emerald-500 dark:hover:text-emerald-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> </svg> <span class="hidden sm:inline">Helpful</span> </button> <button type="button" hx-post="/forums/post/1664839/react/" hx-vals='{"type": "thanks"}' hx-target="#reactions-1664839" hx-swap="outerHTML" title="Thanks for this" class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-amber-500 dark:hover:text-amber-400"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/> </svg> <span class="hidden sm:inline">Thanks</span> </button> </div> <span class="hidden sm:inline text-gray-200 dark:text-gray-700 mx-1">|</span> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="quotePost(1664839, 'bishop_pass')" title="Quote this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg> <span class="hidden sm:inline">Quote</span> </button> <button class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" onclick="copyPostUrl(1664839)" title="Copy link to this post"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <span class="hidden sm:inline">Link</span> </button> </div> </div> </footer> </div> </div> </div> </article> <div id="last-post"></div> </div> <aside class="my-5 overflow-hidden rounded-2xl border border-emerald-200 bg-white shadow-sm transition-all hover:border-emerald-300 hover:shadow-md dark:border-emerald-500/25 dark:bg-gray-900/60 dark:hover:border-emerald-400/50 dark:hover:bg-gray-900/80" data-commerce-surface="forum_topic_endcap" data-commerce-product="3d-environment-design-with-blender-5" data-commerce-reason="product_fallback:global" data-commerce-variant="compact_product" aria-label="Affiliate recommendation" > <a href="/resources/go/3/?surface=forum_topic_endcap" rel="sponsored nofollow noopener" target="_blank" class="group block p-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-950"> <div class="flex gap-4"> <img src="https://m.media-amazon.com/images/I/71N3k+HUY2L._SL1360_.jpg" alt="" loading="lazy" class="h-20 w-20 flex-none rounded-xl bg-gray-100 object-cover shadow-sm ring-1 ring-gray-200 dark:bg-gray-950 dark:ring-white/10"> <div class="min-w-0 flex-1"> <div class="mb-2 flex flex-wrap items-center gap-2"> <span class="rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300">Recommended resource</span> <span class="rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:bg-white/5 dark:text-gray-400">Affiliate</span> </div> <h3 class="text-base font-bold leading-snug text-gray-950 line-clamp-2 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-300">3D Environment Design with Blender 5</h3> <p class="mt-1 text-sm text-gray-500 line-clamp-1 dark:text-gray-400">Amazon &middot; Book</p> <p class="mt-2 text-sm leading-relaxed text-gray-600 line-clamp-2 dark:text-gray-300">Based on Blender 5, this updated second edition simplifies the complexities by guiding you through clear, practical workflows to overcome those challenges. You’ll start by addressing common modeling and proportion mistakes, then move on to realistic texturing and unwrapping to create stunning scenes based on actual references.</p> </div> </div> <div class="mt-4 flex items-center justify-between gap-3"> <p class="min-w-0 text-[11px] leading-snug text-gray-400 dark:text-gray-500">As an Amazon Associate I earn from qualifying purchases.</p> <span class="inline-flex flex-none items-center rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-bold text-white transition-colors group-hover:bg-emerald-700 dark:bg-emerald-400 dark:text-gray-950 dark:group-hover:bg-emerald-300"> View resource <svg class="ml-1.5 h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg> </span> </div> </a> </aside> <nav class="mt-8 flex items-center justify-between"> <div class="text-sm text-gray-500 dark:text-gray-400"> Page 1 of 3 </div> <div class="flex items-center gap-1"> <span class="px-3 py-2 rounded-lg bg-primary-600 text-white text-sm font-medium">1</span> <a href="/forums/topic/121781-this-is-just-wrong?page=2" class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm"> 2 </a> <a href="/forums/topic/121781-this-is-just-wrong?page=3" class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm"> 3 </a> <a href="/forums/topic/121781-this-is-just-wrong?page=2" class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm"> Next → </a> </div> </nav> <section class="mt-8 mb-8" id="reply"> <div class="bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 text-center"> <div class="flex justify-center mb-3"> <div class="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center"> <svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/> </svg> </div> </div> <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">Topic Locked</h3> <p class="text-gray-600 dark:text-gray-400"> This topic has been locked by a moderator. New replies are not allowed. </p> </div> </section> <div class="mt-8 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-center text-gray-600 dark:text-gray-400"> <a href="/login/?next=/forums/topic/121781-this-is-just-wrong/" class="text-primary-600 hover:underline">Sign in</a> to reply to this topic. </div> </div> <aside> <div class="sticky-sidebar space-y-6"> <div class="d-flex flex-column justify-content-center align-items-center mb-3 ads-lazy mpu flex flex-col justify-center items-center mb-3" style="min-height:250px;" data-slot-id="LEGACY_BOX_SIDEBAR" data-tier="0" data-delivery="gam"> <div id="gpt-box-sidebar" class="gpt-ad-slot" data-gpt-path="/1004699/Ad_Box_Sidebar" data-gpt-position="sidebar_top"></div> <div class="text-uppercase advert-label uppercase tracking-wider text-[10px] text-gray-400 dark:text-gray-600 d-none" style="letter-spacing:1px">Advertisement</div> </div> <section class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4"> <h3 class="font-bold text-gray-900 dark:text-white mb-4">Topic Info</h3> <ul class="space-y-3 text-sm"> <li class="flex items-center justify-between text-gray-500 dark:text-gray-400"> <span>Views</span> <span class="font-medium text-gray-900 dark:text-white">750+</span> </li> <li class="flex items-center justify-between text-gray-500 dark:text-gray-400"> <span>Replies</span> <span class="font-medium text-gray-900 dark:text-white">50</span> </li> <li class="flex items-center justify-between text-gray-500 dark:text-gray-400"> <span>Created</span> <span class="font-medium text-gray-900 dark:text-white">Oct 30, 2002</span> </li> <li class="flex items-center justify-between text-gray-500 dark:text-gray-400"> <span>Last Reply</span> <span class="font-medium text-gray-900 dark:text-white">Oct 31, 2002</span> </li> </ul> <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700"> <h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Participants (1)</h4> <div class="flex flex-wrap gap-1"> <a href="/profile/470-michalson" title="Michalson" class="hover:opacity-80 transition-opacity"> <span class="gdx-avatar w-6 h-6 rounded-full "> <span class="gdx-avatar__img gdx-avatar__fallback rounded-full cursor-pointer text-xs" data-user-tooltip data-member-id="470"> <span aria-hidden="true">M</span> </span> </span> </a> </div> </div> </section> <div hx-get="/forums/topic/121781/related/" hx-trigger="revealed" hx-swap="outerHTML"> <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 animate-pulse"> <div class="h-4 w-28 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div> <div class="space-y-3"> <div class="h-3 bg-gray-100 dark:bg-gray-700 rounded"></div> <div class="h-3 w-5/6 bg-gray-100 dark:bg-gray-700 rounded"></div> <div class="h-3 w-4/6 bg-gray-100 dark:bg-gray-700 rounded"></div> </div> </div> </div> <section class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4"> <h3 class="font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.879 16.121A3 3 0 1012.015 11L11 14H9c0 .768.293 1.536.879 2.121z"/> </svg> Popular Topics </h3> <ul class="space-y-2"> <li class="group"> <a href="/forums/topic/720534-kaivern-a-persistent-rpg-built-around-one-living-world-looking-for-professional-feedback-on-technical-feasibility" class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1.5 rounded transition-colors"> <div class="flex items-start gap-1.5"> <span class="text-sm text-gray-900 dark:text-white group-hover:text-sky-600 line-clamp-2"> Kaivern - a persistent RPG built around one living world. Looking for professional feedback on technical feasibility </span> </div> <div class="mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> 7 </span> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> 450+ </span> <span class="truncate">Game Design and Theory</span> </div> </a> </li> <li class="group"> <a href="/forums/topic/720559-multithreading-scheduling-in-game-engines-with-dags" class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1.5 rounded transition-colors"> <div class="flex items-start gap-1.5"> <span class="flex-shrink-0 mt-0.5 text-orange-500 text-xs">🔥</span> <span class="text-sm text-gray-900 dark:text-white group-hover:text-sky-600 line-clamp-2"> Multithreading, scheduling in game engines with DAGs </span> </div> <div class="mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> 5 </span> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> 250+ </span> <span class="truncate">Engines and Middleware</span> </div> </a> </li> <li class="group"> <a href="/forums/topic/720440-ritual-before-work" class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1.5 rounded transition-colors"> <div class="flex items-start gap-1.5"> <span class="text-sm text-gray-900 dark:text-white group-hover:text-sky-600 line-clamp-2"> ritual before work </span> </div> <div class="mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> 5 </span> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> 250+ </span> <span class="truncate">GDNet Lounge</span> </div> </a> </li> <li class="group"> <a href="/forums/topic/720627-space-game" class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1.5 rounded transition-colors"> <div class="flex items-start gap-1.5"> <span class="flex-shrink-0 mt-0.5 text-orange-500 text-xs">🔥</span> <span class="text-sm text-gray-900 dark:text-white group-hover:text-sky-600 line-clamp-2"> Space Game </span> </div> <div class="mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> 5 </span> <span class="flex items-center gap-1"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> 75+ </span> <span class="truncate">For Beginners</span> </div> </a> </li> </ul> </section> <section class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4"> <h3 class="font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <svg class="w-5 h-5 text-sky-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/> </svg> Browse Forums </h3> <nav class="space-y-1"> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">New to GameDev?</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/71-for-beginners" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> For Beginners </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Audio</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/18-music-and-sound-fx" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Music and Sound FX </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Business</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/23-games-business-and-law" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Games Business and Law </a> </li> <li> <a href="/forums/forum/21-games-career-development" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Games Career Development </a> </li> <li> <a href="/forums/forum/22-production-and-management" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Production and Management </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Game Design</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/19-game-design-and-theory" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Game Design and Theory </a> </li> <li> <a href="/forums/forum/20-writing-for-games" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Writing for Games </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Visual Arts</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/12-2d-and-3d-art" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> 2D and 3D Art </a> </li> <li> <a href="/forums/forum/13-art-critique-and-feedback" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Art Critique and Feedback </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Affiliates</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/34-angelcode" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> AngelCode </a> </li> <li> <a href="/forums/forum/33-nehe-productions" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> NeHe Productions </a> </li> </ul> </details> <details class="group" > <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Programming</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/6-artificial-intelligence" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Artificial Intelligence </a> </li> <li> <a href="/forums/forum/10-engines-and-middleware" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Engines and Middleware </a> </li> <li> <a href="/forums/forum/9-general-and-gameplay-programming" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> General and Gameplay Programming </a> </li> <li> <a href="/forums/forum/5-graphics-and-gpu-programming" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Graphics and GPU Programming </a> </li> <li> <a href="/forums/forum/7-math-and-physics" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Math and Physics </a> </li> <li> <a href="/forums/forum/8-networking-and-multiplayer" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Networking and Multiplayer </a> </li> </ul> </details> <details class="group" open> <summary class="flex items-center justify-between cursor-pointer py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="truncate">Community</span> <svg class="w-4 h-4 text-gray-400 group-open:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> </svg> </summary> <ul class="ml-3 mt-1 space-y-0.5 border-l-2 border-gray-100 dark:border-gray-700"> <li> <a href="/forums/forum/82-gamedev-challenges" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> GameDev Challenges </a> </li> <li> <a href="/forums/forum/26-gdnet-comments-suggestions-and-ideas" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> GDNet Comments, Suggestions, and Ideas </a> </li> <li> <a href="/forums/forum/25-gdnet-lounge" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-sky-600 font-medium bg-sky-50 dark:bg-sky-900/20"> GDNet Lounge </a> </li> <li> <a href="/forums/forum/29-hobby-project-classifieds" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Hobby Project Classifieds </a> </li> <li> <a href="/forums/forum/30-indie-showcase" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Indie Showcase </a> </li> <li> <a href="/forums/forum/28-your-announcements" class="block py-1 px-3 text-sm rounded-r hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-600 dark:text-gray-400"> Your Announcements </a> </li> </ul> </details> </nav> </section> <section class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4"> <h3 class="font-bold text-gray-900 dark:text-white mb-4">Quick Actions</h3> <ul class="space-y-2"> <li> <a href="/forums/forum/25-gdnet-lounge" class="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-sky-600 text-sm"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> </svg> Back to GDNet Lounge </a> </li> <li> <a href="/forums/" class="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-sky-600 text-sm"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/> </svg> All Forums </a> </li> </ul> </section> </div> </aside> </div> <div class="mt-8"> <div class="d-flex flex-column justify-content-center align-items-center mb-3 ads-lazy leaderboard flex flex-col justify-center items-center rounded mb-3" style="min-height:90px;" data-slot-id="LEGACY_LEADERBOARD_FOOTER" data-tier="0" data-delivery="gam"> <div id="gpt-leaderboard-footer" class="gpt-ad-slot" data-gpt-path="/1004699/Ad_Leaderboard_Footer" data-gpt-position="footer"></div> <div class="text-uppercase advert-label uppercase tracking-wider text-[10px] text-gray-400 dark:text-gray-600 d-none" style="letter-spacing:1px">Advertisement</div> </div> </div> </div> <script> // Pre-submit quality checks const LOW_EFFORT_PATTERNS = [ /^(thanks|thank you|thx|ty|ok|okay|yes|no|bump|same|this|agreed|\+1|me too|nice|cool|great|good|lol|haha)\.?$/i, /^.{1,15}$/ // Very short replies (less than 15 chars) ]; const SPAM_PATTERNS = [ /(https?:\/\/[^\s]+){3,}/i, // 3+ links /(.)\1{10,}/, // Same character repeated 10+ times /\b(buy|sale|discount|cheap|free|click here|visit|casino|crypto|nft)\b.*\b(buy|sale|discount|cheap|free|click here|visit|casino|crypto|nft)\b/i ]; function checkReplyQuality(content) { const text = content.replace(/<[^>]*>/g, '').trim(); // Strip HTML const issues = []; // Check for low-effort for (const pattern of LOW_EFFORT_PATTERNS) { if (pattern.test(text)) { issues.push({ type: 'low_effort', message: 'Your reply seems brief. Consider adding more detail to help the community.' }); break; } } // Check for spam patterns for (const pattern of SPAM_PATTERNS) { if (pattern.test(text)) { issues.push({ type: 'spam', message: 'Your reply contains patterns that may be flagged as spam.' }); break; } } // Check for empty content if (text.length === 0) { issues.push({ type: 'empty', message: 'Please write something before posting.' }); } return issues; } function showQualityNudge(issues, onContinue) { const modal = document.getElementById('quality-nudge-modal'); const issuesList = document.getElementById('quality-issues-list'); issuesList.innerHTML = issues.map(i => `<li class="flex items-start gap-2 text-amber-700 dark:text-amber-300"> <svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> </svg> <span>${i.message}</span> </li>` ).join(''); // Store callback for continue button window._qualityNudgeContinue = onContinue; modal.classList.remove('hidden'); } function closeQualityNudge() { document.getElementById('quality-nudge-modal').classList.add('hidden'); } function continuePostingAnyway() { closeQualityNudge(); if (window._qualityNudgeContinue) { window._qualityNudgeContinue(); } } // Sync TipTap editor content before form submit document.addEventListener('DOMContentLoaded', function() { const replyForm = document.querySelector('#reply-form'); if (replyForm) { // Sync editor content before form submit replyForm.addEventListener('submit', function(e) { if (window.gamedevEditors && window.gamedevEditors.length > 0) { window.gamedevEditors[0].syncToHiddenFields(); } }); // Also handle HTMX requests with quality check replyForm.addEventListener('htmx:beforeRequest', function(e) { if (window.gamedevEditors && window.gamedevEditors.length > 0) { window.gamedevEditors[0].syncToHiddenFields(); } // Quality check (skip if already bypassed) if (!replyForm.dataset.qualityBypassed) { const content = document.getElementById('content_html')?.value || ''; const issues = checkReplyQuality(content); // Only block for non-empty issues (empty is always blocked) const blockingIssues = issues.filter(i => i.type !== 'empty' || issues.length === 1); if (blockingIssues.length > 0 && blockingIssues[0].type !== 'empty') { e.preventDefault(); showQualityNudge(blockingIssues, function() { replyForm.dataset.qualityBypassed = 'true'; htmx.trigger(replyForm, 'submit'); delete replyForm.dataset.qualityBypassed; }); return false; } } }); // Handle successful reply submission replyForm.addEventListener('htmx:afterRequest', function(e) { if (e.detail.successful) { // Small delay to ensure DOM is ready setTimeout(function() { clearReplyEditor(); showToast('Reply posted!', 'success'); }, 100); } }); } }); // Clear editor and reset form function clearReplyEditor() { if (window.gamedevEditors && window.gamedevEditors.length > 0) { const gdEditor = window.gamedevEditors[0]; // Clear editor content if (gdEditor.editor) { gdEditor.editor.commands.setContent('', false); } // Clear autosave draft using the editor's method if (typeof gdEditor.clearDraft === 'function') { gdEditor.clearDraft(); } else if (gdEditor.autosaveKey) { localStorage.removeItem(gdEditor.autosaveKey); } // Sync to hidden field if (typeof gdEditor.syncToHiddenFields === 'function') { gdEditor.syncToHiddenFields(); } } // Also directly clear the hidden field const contentField = document.getElementById('content_html'); if (contentField) { contentField.value = ''; } } function quotePost(postId, authorName) { // Get selected text if any let textToQuote = window.getSelection().toString().trim(); // If no selection, get the post content if (!textToQuote && postId) { const postContentEl = document.getElementById(`post-content-${postId}`); if (postContentEl) { // Get text content, clean up whitespace textToQuote = postContentEl.innerText.trim().substring(0, 500); if (postContentEl.innerText.trim().length > 500) { textToQuote += '...'; } } } // Try to insert into TipTap editor if (window.gamedevEditors && window.gamedevEditors.length > 0) { const editor = window.gamedevEditors[0].editor; if (editor) { const content = `<blockquote><p><strong>${authorName}</strong> wrote:</p><p>${textToQuote || ''}</p></blockquote><p></p>`; editor.commands.insertContent(content); editor.commands.focus('end'); document.getElementById('reply')?.scrollIntoView({ behavior: 'smooth' }); return; } } // Fallback to textarea const textarea = document.getElementById('reply-content'); if (textarea && textarea.tagName === 'TEXTAREA') { const formattedQuote = textToQuote ? `> **${authorName}** wrote:\n> ${textToQuote.replace(/\n/g, '\n> ')}\n\n` : `@${authorName} `; textarea.value += formattedQuote; textarea.focus(); textarea.scrollIntoView({ behavior: 'smooth' }); } } function copyPostUrl(postId) { const url = window.location.origin + window.location.pathname + '#post-' + postId; navigator.clipboard.writeText(url).then(() => { // Show brief feedback const btn = event.target instanceof Element ? event.target.closest('button') : event.target.parentElement; const originalTitle = btn.title; btn.title = 'Copied!'; setTimeout(() => { btn.title = originalTitle; }, 2000); }); } // Floating quote selection popup (function() { let quotePopup = null; let currentPostId = null; let currentAuthorName = null; let currentSelectedText = null; // Store the selected text function createQuotePopup() { const popup = document.createElement('div'); popup.id = 'quote-selection-popup'; popup.style.cssText = 'position: fixed; z-index: 9999; display: none;'; popup.innerHTML = ` <button class="flex items-center gap-1.5 px-3 py-1.5 bg-gray-800 dark:bg-gray-700 text-white text-sm rounded-lg shadow-lg hover:bg-gray-700 dark:hover:bg-gray-600 transition-colors border border-gray-600"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/> </svg> Quote </button> `; popup.querySelector('button').addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); if (currentPostId !== null && currentAuthorName && currentSelectedText) { insertQuote(currentAuthorName, currentSelectedText); } hideQuotePopup(); }); document.body.appendChild(popup); return popup; } function insertQuote(authorName, text) { // Try to insert into TipTap editor if (window.gamedevEditors && window.gamedevEditors.length > 0) { const editor = window.gamedevEditors[0].editor; if (editor) { const content = `<blockquote><p><strong>${authorName}</strong> wrote:</p><p>${text}</p></blockquote><p></p>`; editor.commands.insertContent(content); editor.commands.focus('end'); document.getElementById('reply')?.scrollIntoView({ behavior: 'smooth' }); return; } } // Fallback to textarea const textarea = document.getElementById('reply-content'); if (textarea && textarea.tagName === 'TEXTAREA') { textarea.value += `> **${authorName}** wrote:\n> ${text.replace(/\n/g, '\n> ')}\n\n`; textarea.focus(); textarea.scrollIntoView({ behavior: 'smooth' }); } } function showQuotePopup(x, y, postId, authorName, selectedText) { if (!quotePopup) quotePopup = createQuotePopup(); currentPostId = postId; currentAuthorName = authorName; currentSelectedText = selectedText; // Position popup above selection quotePopup.style.left = `${Math.max(10, x)}px`; quotePopup.style.top = `${Math.max(10, y - 45)}px`; quotePopup.style.display = 'block'; } function hideQuotePopup() { if (quotePopup) { quotePopup.style.display = 'none'; } currentPostId = null; currentAuthorName = null; currentSelectedText = null; } // Listen for text selection in posts document.addEventListener('mouseup', (e) => { // Small delay to let selection finalize setTimeout(() => { const selection = window.getSelection(); if (!selection.rangeCount) { hideQuotePopup(); return; } const text = selection.toString().trim(); if (text.length < 10) { hideQuotePopup(); return; } // Check if selection is within a post content area const range = selection.getRangeAt(0); let node = range.commonAncestorContainer; if (node.nodeType === Node.TEXT_NODE) { node = node.parentElement; } // Find the post article element const postEl = node.closest('article[id^="post-"]'); if (!postEl) { hideQuotePopup(); return; } // Get post ID and author name from data attribute const postId = postEl.id.replace('post-', ''); const authorName = postEl.dataset.authorName || 'Unknown'; // Get selection coordinates const rect = range.getBoundingClientRect(); const x = rect.left + (rect.width / 2) - 40; const y = rect.top; // Store the selected text NOW before it gets cleared showQuotePopup(x, y, postId, authorName, text); }, 50); }); // Hide popup when clicking elsewhere document.addEventListener('mousedown', (e) => { if (quotePopup && !quotePopup.contains(e.target)) { hideQuotePopup(); } }); })(); // Mod drawer functions (staff and moderators) function openModDrawer() { const drawer = document.getElementById('mod-drawer'); drawer.classList.remove('translate-x-full'); // Load content via HTMX htmx.ajax('GET', '/forums/topic/121781/mod-notes/', {target: '#mod-drawer-content', swap: 'innerHTML'}); } function closeModDrawer() { document.getElementById('mod-drawer').classList.add('translate-x-full'); } // Post mod menu functions function openPostModMenu(postId) { const modal = document.getElementById('post-mod-modal'); const postIdInput = document.getElementById('post-mod-post-id'); postIdInput.value = postId; document.getElementById('post-mod-id-display').textContent = postId; modal.classList.remove('hidden'); } function closePostModMenu() { document.getElementById('post-mod-modal').classList.add('hidden'); } function executePostModAction(action) { const postId = document.getElementById('post-mod-post-id').value; fetch('/moderation/action/post/' + postId + '/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value }, body: 'action=' + action }).then(() => { closePostModMenu(); window.location.reload(); }); } </script> <div class="hidden md:block fixed bottom-4 right-4 z-10"> <button onclick="document.getElementById('shortcuts-modal').classList.remove('hidden')" class="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded shadow-sm border border-gray-200 dark:border-gray-700"> Press <kbd class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-gray-600 dark:text-gray-300">?</kbd> for shortcuts </button> </div> <script src="/static/gamedevnext/js/forum-shortcuts.a18f66b1e18e.js"></script> <script> /** * Toggle like on content * @param {string} contentType - Content type identifier * @param {number} contentId - Content ID * @param {HTMLElement} btn - Button element */ function toggleLike(contentType, contentId, btn) { if (btn.dataset.pending === 'true') return; btn.dataset.pending = 'true'; btn.disabled = true; const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.querySelector('meta[name="csrf-token"]')?.content || document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1] || ''; fetch(`/api/content/${contentType}/${contentId}/like/`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { if (data.message === 'OK') { const svg = btn.querySelector('svg'); const countSpan = btn.querySelector('.like-count'); if (data.vote > 0) { // Liked btn.classList.remove('text-gray-500', 'hover:text-pink-600', 'hover:bg-pink-50', 'dark:text-gray-400', 'dark:hover:text-pink-400', 'dark:hover:bg-pink-900/20'); btn.classList.add('bg-pink-100', 'text-pink-600', 'dark:bg-pink-900/30', 'dark:text-pink-400'); svg.classList.add('fill-current'); svg.setAttribute('fill', 'currentColor'); btn.title = 'Unlike'; } else { // Unliked btn.classList.add('text-gray-500', 'hover:text-pink-600', 'hover:bg-pink-50', 'dark:text-gray-400', 'dark:hover:text-pink-400', 'dark:hover:bg-pink-900/20'); btn.classList.remove('bg-pink-100', 'text-pink-600', 'dark:bg-pink-900/30', 'dark:text-pink-400'); svg.classList.remove('fill-current'); svg.setAttribute('fill', 'none'); btn.title = 'Like'; } if (countSpan && data.total !== undefined) { countSpan.textContent = data.total; } } else if (data.message) { // Show message (rate limit, self-like, etc.) showToast(data.message, 'info'); } else if (data.error) { showToast(data.error, 'error'); } }) .catch(err => { console.error('Like error:', err); showToast('Failed to update like', 'error'); }) .finally(() => { btn.dataset.pending = 'false'; btn.disabled = false; }); } /** * Toggle bookmark on content * @param {string} contentType - Content type identifier * @param {number} contentId - Content ID * @param {HTMLElement} btn - Button element */ function toggleBookmark(contentType, contentId, btn) { const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.querySelector('meta[name="csrf-token"]')?.content || document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1] || ''; fetch(`/api/content/${contentType}/${contentId}/bookmark/`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { if (data.bookmarked !== undefined) { const svg = btn.querySelector('svg'); const labelSpan = btn.querySelector('.bookmark-label'); if (data.bookmarked) { // Bookmarked btn.classList.remove('text-gray-500', 'hover:text-amber-600', 'hover:bg-amber-50', 'dark:text-gray-400', 'dark:hover:text-amber-400', 'dark:hover:bg-amber-900/20'); btn.classList.add('bg-amber-100', 'text-amber-600', 'dark:bg-amber-900/30', 'dark:text-amber-400'); svg.classList.add('fill-current'); svg.setAttribute('fill', 'currentColor'); btn.title = 'Remove bookmark'; if (labelSpan) labelSpan.textContent = 'Saved'; } else { // Unbookmarked btn.classList.add('text-gray-500', 'hover:text-amber-600', 'hover:bg-amber-50', 'dark:text-gray-400', 'dark:hover:text-amber-400', 'dark:hover:bg-amber-900/20'); btn.classList.remove('bg-amber-100', 'text-amber-600', 'dark:bg-amber-900/30', 'dark:text-amber-400'); svg.classList.remove('fill-current'); svg.setAttribute('fill', 'none'); btn.title = 'Save bookmark'; if (labelSpan) labelSpan.textContent = 'Save'; } } else if (data.error) { showToast(data.error, 'error'); } }) .catch(err => { console.error('Bookmark error:', err); showToast('Failed to update bookmark', 'error'); }); } /** * Simple toast notification (uses existing toast if available, else alert) */ function showToast(message, type) { // Check for existing toast system if (typeof window.Toast !== 'undefined') { window.Toast.show(message, type); } else if (typeof window.showNotification !== 'undefined') { window.showNotification(message, type); } else { // Fallback to a simple toast const toast = document.createElement('div'); toast.className = `fixed bottom-4 right-4 px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity duration-300 ${type === 'error' ? 'bg-red-600 text-white' : 'bg-gray-800 text-white dark:bg-gray-700'}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.classList.add('opacity-0'); setTimeout(() => toast.remove(), 300); }, 3000); } } </script> <!-- Report Modal --> <div id="report-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 99999; align-items: center; justify-content: center;" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <!-- Background overlay --> <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5);" onclick="closeReportModal()"></div> <!-- Modal panel --> <div style="position: relative; z-index: 100000; background: white; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);" class="dark:bg-gray-800"> <form id="report-form" onsubmit="submitReport(event)"> <div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div class="sm:flex sm:items-start"> <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 sm:mx-0 sm:h-10 sm:w-10"> <svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> </svg> </div> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1"> <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title"> Report Content </h3> <div class="mt-4"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> Reason for reporting </label> <select name="reason" id="report-reason" required class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500"> <option value="">Select a reason...</option> <option value="spam">Spam or advertising</option> <option value="harassment">Harassment or abuse</option> <option value="inappropriate">Inappropriate content</option> <option value="off_topic">Off-topic or irrelevant</option> <option value="misinformation">Misinformation</option> <option value="copyright">Copyright violation</option> <option value="other">Other</option> </select> </div> <div class="mt-4"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" id="report-comment-label"> Additional details (optional) </label> <textarea name="comment" id="report-comment" rows="3" maxlength="500" placeholder="Please provide any additional context..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500"></textarea> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400" id="report-comment-help">Max 500 characters</p> </div> </div> </div> </div> <div class="bg-gray-50 dark:bg-gray-700/50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-2"> <button type="submit" id="report-submit-btn" class="w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed"> Submit Report </button> <button type="button" onclick="closeReportModal()" class="mt-3 w-full inline-flex justify-center rounded-lg border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:mt-0 sm:w-auto sm:text-sm"> Cancel </button> </div> <!-- Hidden fields for content identification --> <input type="hidden" id="report-content-type" name="content_type" value=""> <input type="hidden" id="report-content-id" name="content_id" value=""> </form> </div> </div> </div> <script> let reportModalOpen = false; // Must mirror ContentReport.DETAIL_REQUIRED_REASONS / MIN_DETAIL_LENGTH. // These reasons are meaningless to a moderator on their own, so the reporter // has to explain what happened. The server enforces this too. const REPORT_DETAIL_REQUIRED_REASONS = ['other', 'harassment']; const REPORT_MIN_DETAIL_LENGTH = 20; function reportDetailRequired(reason) { return REPORT_DETAIL_REQUIRED_REASONS.includes(reason); } function updateReportDetailRequirement() { const reason = document.getElementById('report-reason').value; const label = document.getElementById('report-comment-label'); const help = document.getElementById('report-comment-help'); const comment = document.getElementById('report-comment'); if (reportDetailRequired(reason)) { label.textContent = 'What happened? (required)'; help.textContent = `Please give at least ${REPORT_MIN_DETAIL_LENGTH} characters so a moderator can act on this.`; comment.setAttribute('required', 'required'); comment.placeholder = 'Describe what this user did, and where it happened...'; } else { label.textContent = 'Additional details (optional)'; help.textContent = 'Max 500 characters'; comment.removeAttribute('required'); comment.placeholder = 'Please provide any additional context...'; } } function openReportModal(contentType, contentId) { const modal = document.getElementById('report-modal'); if (!modal) { console.error('Report modal element not found!'); return; } document.getElementById('report-content-type').value = contentType; document.getElementById('report-content-id').value = contentId; document.getElementById('report-reason').value = ''; document.getElementById('report-comment').value = ''; document.getElementById('modal-title').textContent = contentType === 'user' ? 'Report User' : 'Report Content'; updateReportDetailRequirement(); modal.style.display = 'flex'; reportModalOpen = true; } function closeReportModal() { const modal = document.getElementById('report-modal'); modal.style.display = 'none'; reportModalOpen = false; } function submitReport(event) { event.preventDefault(); const contentType = document.getElementById('report-content-type').value; const contentId = document.getElementById('report-content-id').value; const reason = document.getElementById('report-reason').value; const comment = document.getElementById('report-comment').value; const submitBtn = document.getElementById('report-submit-btn'); if (!reason) { showToast('Please select a reason for reporting', 'error'); return; } if (reportDetailRequired(reason) && comment.trim().length < REPORT_MIN_DETAIL_LENGTH) { showToast( `Please describe what happened (at least ${REPORT_MIN_DETAIL_LENGTH} characters) for this reason.`, 'error' ); return; } submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.querySelector('meta[name="csrf-token"]')?.content; fetch(`/api/content/${contentType}/${contentId}/report/`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', }, body: JSON.stringify({ reason, comment }) }) .then(response => response.json()) .then(data => { if (data.reported) { closeReportModal(); showToast(data.message, 'success'); } else { // Keep the modal open on a validation error so the reporter can // fix their submission instead of losing what they typed. showToast(data.error || data.message || 'Failed to submit report', 'error'); if (!data.error) { closeReportModal(); } } }) .catch(err => { console.error('Report error:', err); showToast('Failed to submit report', 'error'); }) .finally(() => { submitBtn.disabled = false; submitBtn.textContent = 'Submit Report'; }); } document.addEventListener('DOMContentLoaded', function() { const reasonSelect = document.getElementById('report-reason'); if (reasonSelect) { reasonSelect.addEventListener('change', updateReportDetailRequirement); } }); // Close on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && reportModalOpen) { closeReportModal(); } }); </script> <script> (function() { const MAX_QUOTE_HEIGHT = 150; // pixels before collapse function isEmptyNode(node) { if (node.nodeType === Node.TEXT_NODE) { return !node.textContent.replace(/\u00a0/g, ' ').trim(); } if (node.nodeType !== Node.ELEMENT_NODE) return true; if (node.tagName === 'BR') return true; const text = (node.textContent || '').replace(/\u00a0/g, ' ').trim(); if (text) return false; if (node.querySelector && node.querySelector('img, video, iframe, svg, audio')) return false; return true; } function trimTrailingEmpty(el) { while (el.lastChild && isEmptyNode(el.lastChild)) { el.removeChild(el.lastChild); } } function initCollapsibleQuotes() { document.querySelectorAll('.prose blockquote, .post-content blockquote').forEach(quote => { // Skip if already processed if (quote.dataset.collapsible) return; // Skip blockquotes inside editors (TipTap/ProseMirror) to avoid DOM mutation loops if (quote.closest('[contenteditable], .ProseMirror, .gamedev-editor-content, [data-tiptap], [data-tiptap-inline]')) return; quote.dataset.collapsible = 'true'; // Strip trailing empty paragraphs / <br> / whitespace so height reflects real content trimTrailingEmpty(quote); // Measure actual height const originalHeight = quote.scrollHeight; if (originalHeight > MAX_QUOTE_HEIGHT) { // Add collapsed state quote.style.maxHeight = MAX_QUOTE_HEIGHT + 'px'; quote.style.overflow = 'hidden'; quote.classList.add('quote-collapsed'); // Create expand button const expandBtn = document.createElement('button'); expandBtn.type = 'button'; expandBtn.className = 'quote-expand-btn mt-2 flex w-full items-center justify-center py-2 text-xs font-medium text-sky-600 dark:text-sky-400 hover:text-sky-700 dark:hover:text-sky-300 transition-colors'; expandBtn.innerHTML = ` <span class="flex items-center justify-center gap-1"> <svg class="w-4 h-4 expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> </svg> <span class="expand-text">Show more</span> </span> `; expandBtn.addEventListener('click', function(e) { e.preventDefault(); const isCollapsed = quote.classList.contains('quote-collapsed'); if (isCollapsed) { quote.style.maxHeight = originalHeight + 'px'; quote.classList.remove('quote-collapsed'); quote.classList.add('quote-expanded'); expandBtn.querySelector('.expand-text').textContent = 'Show less'; expandBtn.querySelector('.expand-icon').style.transform = 'rotate(180deg)'; } else { quote.style.maxHeight = MAX_QUOTE_HEIGHT + 'px'; quote.classList.add('quote-collapsed'); quote.classList.remove('quote-expanded'); expandBtn.querySelector('.expand-text').textContent = 'Show more'; expandBtn.querySelector('.expand-icon').style.transform = 'rotate(0deg)'; } }); // Add wrapper for positioning const wrapper = document.createElement('div'); wrapper.className = 'quote-wrapper flex flex-col'; quote.parentNode.insertBefore(wrapper, quote); wrapper.appendChild(quote); wrapper.appendChild(expandBtn); } }); } // Run on page load document.addEventListener('DOMContentLoaded', initCollapsibleQuotes); // Re-run after HTMX swaps only for post content areas (not sidebar/nav updates) document.body.addEventListener('htmx:afterSwap', function(e) { var target = e.detail.target || e.target; if (target instanceof Element && (target.id === 'posts-list' || target.closest('#posts-list') || target.classList.contains('post-content'))) { initCollapsibleQuotes(); } }); })(); </script> <style> .quote-wrapper { position: relative; } .quote-wrapper blockquote { transition: max-height 0.3s ease-out; } .quote-wrapper blockquote.quote-collapsed { margin-bottom: 0; } .quote-expand-btn { cursor: pointer; border: none; background: transparent; } .quote-expand-btn .expand-icon { transition: transform 0.2s ease; } </style> </main> </div> <!-- Tailwind Footer - Full width --> <footer class="bg-gray-900 text-gray-300 w-full"> <div class="site-shell py-12"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8"> <!-- Brand --> <div class="md:col-span-1"> <a href="/" class="text-2xl font-bold text-white">GameDev<span class="text-sky-500">.net</span></a> <p class="mt-3 text-sm text-gray-400">The game development community since 1999. Learn, share, and connect with developers worldwide.</p> </div> <!-- Quick Links --> <div> <h4 class="font-semibold text-white mb-4">Community</h4> <ul class="space-y-2 text-sm"> <li><a href="/forums/" class="hover:text-white transition">Forums</a></li> <li><a href="/blogs/" class="hover:text-white transition">Blogs</a></li> <li><a href="/projects/" class="hover:text-white transition">Projects</a></li> <li><a href="/portfolios/" class="hover:text-white transition">Portfolios</a></li> <li><a href="/badges/" class="hover:text-white transition">Badges</a></li> </ul> </div> <!-- Resources --> <div> <h4 class="font-semibold text-white mb-4">Resources</h4> <ul class="space-y-2 text-sm"> <li><a href="/tutorials/" class="hover:text-white transition">Tutorials</a></li> <li><a href="/news/" class="hover:text-white transition">News</a></li> <li><a href="/start-game-development/" class="hover:text-white transition">How to Make a Game</a></li> <li><a href="/guide/" class="hover:text-white transition">Getting Started Guide</a></li> <li><a href="/guilds/" class="hover:text-white transition">Topics & Guilds</a></li> <li><a href="/pro/" class="hover:text-white transition">GameDev Pro</a></li> <li><a href="/developers/mcp/" class="hover:text-white transition">Developers</a></li> </ul> </div> <!-- Legal --> <div> <h4 class="font-semibold text-white mb-4">About & Legal</h4> <ul class="space-y-2 text-sm"> <li><a href="/guidelines/" class="hover:text-white transition">Community Guidelines</a></li> <li><a href="/terms/" class="hover:text-white transition">Terms of Service</a></li> <li><a href="/privacy/" class="hover:text-white transition">Privacy Policy</a></li> <li><a href="/licensing/" class="hover:text-white transition">Content Licensing</a></li> <li><a href="/contact/" class="hover:text-white transition">Contact Us</a></li> <li><a href="/about/" class="hover:text-white transition">About</a></li> <li><a href="/changelog/" class="hover:text-white transition">Changelog</a></li> </ul> </div> </div> <!-- Social Links --> <div class="border-t border-gray-800 mt-10 pt-6"> <div class="flex flex-col md:flex-row justify-between items-center gap-4"> <p class="text-sm text-gray-500">&copy; 1999-2026 GameDev.net, LLC. All rights reserved.</p> <div class="flex items-center gap-3"> <a href="https://discord.gg/tgEGVNy" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="Discord"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg> </a> <a href="https://twitter.com/gdevnet" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="X / Twitter"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> </a> <a href="https://www.facebook.com/GameDev.net/" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="Facebook"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> </a> <a href="https://instagram.com/gdevnet" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="Instagram"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z"/></svg> </a> <a href="https://threads.net/gdevnet" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="Threads"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.59 12c.025 3.086.718 5.496 2.057 7.164 1.432 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.042 1.259-1.233 1.885-2.742 1.86-4.486-.016-1.146-.348-2.143-.988-2.962-.587-.752-1.357-1.18-2.254-1.205a2.559 2.559 0 0 0-.793.062c.135.478.2.976.188 1.48-.046 2.02-1.2 3.51-3.01 3.885a4.066 4.066 0 0 1-.88.098c-1.2 0-2.255-.504-2.975-1.418-.6-.762-.907-1.726-.907-2.852 0-1.893.827-3.544 2.268-4.522a5.126 5.126 0 0 1 2.91-.901c.63.006 1.237.1 1.816.282.26-.586.652-1.076 1.164-1.44a3.487 3.487 0 0 1 2-.614h.076c1.478.025 2.789.698 3.692 1.893 1.007 1.333 1.52 3.092 1.527 5.238.026 2.387-.87 4.505-2.594 6.125C17.904 23.14 15.413 23.98 12.186 24zM11.29 11.083c-.885.386-1.465 1.348-1.465 2.56 0 1.097.528 1.93 1.406 2.222.216.072.445.108.681.108.985 0 1.804-.66 1.91-1.553a2.84 2.84 0 0 0-.037-.793 3.86 3.86 0 0 0-.18-.602 3.592 3.592 0 0 0-2.315-1.942z"/></svg> </a> <a href="https://reddit.com/r/gamedevnet" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-white transition" title="Reddit"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg> </a> <span class="w-px h-4 bg-gray-700"></span> <a href="/rss/" target="_blank" class="text-gray-400 hover:text-white transition" title="RSS Feed"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795 0 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.742-7.115-15.793-15.839-15.82zm0-8.18v4.819c12.951.115 23.412 10.617 23.5 23.581h4.5c-.115-15.452-12.652-27.988-28-28.4z"/></svg> </a> <span class="w-px h-4 bg-gray-700"></span> <a href="#" class="text-sm text-gray-400 hover:text-white transition flex items-center gap-1"> <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd"/></svg> Back to Top </a> </div> </div> </div> </div> </footer> <script>(function(){var darkIcon=document.getElementById('theme-toggle-dark-icon');var lightIcon=document.getElementById('theme-toggle-light-icon');var systemIcon=document.getElementById('theme-toggle-system-icon');var btn=document.getElementById('theme-toggle');var stored=localStorage.getItem('color-theme');function hideAllIcons(){darkIcon.classList.add('hidden');lightIcon.classList.add('hidden');systemIcon.classList.add('hidden');} function showIcon(){hideAllIcons();if(!stored){systemIcon.classList.remove('hidden');btn.title='Theme: Auto (system)';}else if(stored==='dark'){lightIcon.classList.remove('hidden');btn.title='Theme: Dark';}else{darkIcon.classList.remove('hidden');btn.title='Theme: Light';}} function applyTheme(){if(stored==='dark'||(!stored&&window.matchMedia('(prefers-color-scheme: dark)').matches)){document.documentElement.classList.add('dark');}else{document.documentElement.classList.remove('dark');}} showIcon();btn.addEventListener('click',function(){if(!stored){stored='light';localStorage.setItem('color-theme','light');}else if(stored==='light'){stored='dark';localStorage.setItem('color-theme','dark');}else{stored=null;localStorage.removeItem('color-theme');} applyTheme();showIcon();if(!stored){window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',function handler(e){if(localStorage.getItem('color-theme')){e.target.removeEventListener('change',handler);return;} applyTheme();});}});})();;</script> <script src="https://unpkg.com/htmx.org@2.0.4"></script> <script> // Global HTMX CSRF — ensures all HTMX POST/PUT/DELETE requests include Django's CSRF token document.body.addEventListener('htmx:configRequest', function(evt) { var token = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.cookie.split('; ').find(function(r){return r.startsWith('csrftoken=')})?.split('=')[1] || ''; if (token) evt.detail.headers['X-CSRFToken'] = token; }); </script> <script src="https://cdn.jsdelivr.net/npm/fslightbox@3.4.2/index.min.js"></script> <script src="/static/gamedevnext/js/content-lightbox.4ca339e55408.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { // Find all oembed elements and convert them to actual embeds document.querySelectorAll('figure.media > oembed, oembed').forEach(function(item) { var url = item.getAttribute('url'); if (!url) return; fetch('/embed/get/?url=' + encodeURIComponent(url), { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { if (data.preview) { // Replace the oembed element or its parent figure var parent = item.closest('figure.media') || item.parentElement; if (parent) { parent.replaceWith(data.preview); } else { item.replaceWith(data.preview); } } }) .catch(err => console.warn('Failed to load embed:', url, err)); }); }); </script> <script src="/static/gamedevnext/js/user-tooltip.08c872581492.js"></script> <script src="/static/gamedevnext/js/shader-tiles.3e9477d364a5.js" defer></script> <script src="/static/bundles/runtime-5781e36a.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-02e12e81-2ba9b67b.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-839f78ca-54605f01.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-631f0d8c-7e852c16.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-da055998-064d2c61.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-f0157c14-f6130f88.js" ></script> <script src="/static/bundles/tiptap-gdx-vendors-d2d18b07-0d4f8bbf.js" ></script> <script src="/static/bundles/7515-625c095f.js" ></script> <script src="/static/bundles/8339-b65c7b02.js" ></script> <script src="/static/bundles/803-6483b301.js" ></script> <script src="/static/bundles/2845-52745896.js" ></script> <script src="/static/bundles/8262-7d245816.js" ></script> <script src="/static/bundles/914-e576fc61.js" ></script> <script src="/static/bundles/3801-9b9f5dd6.js" ></script> <link href="/static/bundles/message_editor-d1be09fe.css" rel="stylesheet" /> <script src="/static/bundles/message_editor-c58f8947.js" ></script> <!-- Beta Feedback Modal --> <div id="beta-feedback-modal" class="fixed inset-0 z-[100] hidden" role="dialog" aria-modal="true" aria-labelledby="beta-feedback-title" onclick="if(event.target === this) closeBetaFeedback()"> <div class="fixed inset-0 bg-black/50 backdrop-blur-sm" aria-hidden="true"></div> <div class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md"> <div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl p-6 m-4" id="beta-feedback-dialog"> <div class="flex items-center justify-between mb-4"> <h3 id="beta-feedback-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2"> <svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/> </svg> Beta Feedback </h3> <button onclick="closeBetaFeedback()" aria-label="Close feedback dialog" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500 rounded"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> </svg> </button> </div> <div id="beta-feedback-content"> <form id="beta-feedback-form" hx-post="/api/beta/feedback/" hx-target="#beta-feedback-content" hx-swap="innerHTML" hx-indicator="#feedback-spinner"> <input type="hidden" name="csrfmiddlewaretoken" value="pNSQOuwWL7rCKKNMsVsutdvgVbTL2L931gKL1bhy2fBi6fUXbdD8cMQCenTu3N0J"> <input type="hidden" name="page_url" id="feedback-page-url"> <input type="hidden" name="browser_info" id="feedback-browser-info"> <div class="mb-4"> <label for="feedback-type" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Type</label> <select name="feedback_type" id="feedback-type" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:outline-none"> <option value="bug">🐛 Bug Report</option> <option value="suggestion">💡 Suggestion</option> <option value="question">❓ Question</option> <option value="other">📝 Other</option> </select> </div> <div class="mb-4"> <label for="feedback-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> Description <span class="text-gray-400 font-normal">(max 5000 chars)</span> </label> <textarea name="description" id="feedback-description" rows="4" required maxlength="5000" placeholder="Describe the issue or feedback..." class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 placeholder-gray-400 resize-none focus:outline-none"></textarea> </div> <div class="mb-4"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> Screenshot <span class="text-gray-400 font-normal">(optional)</span> </label> <input type="hidden" name="screenshot_url" id="feedback-screenshot-url"> <div id="screenshot-preview" class="hidden mb-2"> <div class="relative inline-block"> <img id="screenshot-img" class="max-h-32 rounded border border-gray-300 dark:border-gray-600"> <button type="button" onclick="clearScreenshot()" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> </svg> </button> </div> </div> <div id="screenshot-buttons" class="flex gap-2"> <button type="button" onclick="captureScreenshot()" class="px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-1.5"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/> </svg> Capture Screen </button> <label class="px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-1.5 cursor-pointer"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/> </svg> Upload Image <input type="file" accept="image/png,image/jpeg,image/webp" class="hidden" onchange="uploadScreenshot(this)"> </label> </div> <p id="screenshot-status" class="text-xs text-gray-500 mt-1 hidden"></p> </div> <div class="flex justify-end gap-2"> <button type="button" onclick="closeBetaFeedback()" class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"> Cancel </button> <button type="submit" class="px-4 py-2 text-sm bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors flex items-center gap-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"> <span id="feedback-spinner" class="htmx-indicator"> <svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> Submit Feedback </button> </div> </form> </div> </div> </div> </div> <script> (function() { let previousActiveElement = null; const feedbackContentEl = document.getElementById('beta-feedback-content'); const originalFeedbackHTML = feedbackContentEl ? feedbackContentEl.innerHTML : null; window.openBetaFeedback = function() { previousActiveElement = document.activeElement; // Restore form HTML first if it was replaced by HTMX submission if (feedbackContentEl && originalFeedbackHTML && !document.getElementById('feedback-page-url')) { feedbackContentEl.innerHTML = originalFeedbackHTML; if (typeof htmx !== 'undefined') htmx.process(feedbackContentEl); } const pageUrlEl = document.getElementById('feedback-page-url'); const browserInfoEl = document.getElementById('feedback-browser-info'); if (pageUrlEl) pageUrlEl.value = window.location.href; if (browserInfoEl) browserInfoEl.value = navigator.userAgent; const modal = document.getElementById('beta-feedback-modal'); if (!modal) return; modal.classList.remove('hidden'); document.body.classList.add('overflow-hidden'); // Focus first input after modal opens setTimeout(() => { const ft = document.getElementById('feedback-type'); if (ft) ft.focus(); }, 50); }; window.closeBetaFeedback = function() { const modal = document.getElementById('beta-feedback-modal'); modal.classList.add('hidden'); document.body.classList.remove('overflow-hidden'); // Return focus to trigger element if (previousActiveElement) previousActiveElement.focus(); // Restore original form HTML after a delay (HTMX replaces it on submit) setTimeout(() => { if (feedbackContentEl && originalFeedbackHTML) { feedbackContentEl.innerHTML = originalFeedbackHTML; htmx.process(feedbackContentEl); } }, 300); }; // Close on Escape key (only when modal is open) document.addEventListener('keydown', function(e) { const modal = document.getElementById('beta-feedback-modal'); if (e.key === 'Escape' && modal && !modal.classList.contains('hidden')) { closeBetaFeedback(); } }); // Focus trap for modal document.addEventListener('keydown', function(e) { const modal = document.getElementById('beta-feedback-modal'); if (!modal || modal.classList.contains('hidden') || e.key !== 'Tab') return; const dialog = document.getElementById('beta-feedback-dialog'); const focusable = dialog.querySelectorAll('button, select, textarea, [tabindex]:not([tabindex="-1"])'); const first = focusable[0], last = focusable[focusable.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); // Screenshot functions window.captureScreenshot = async function() { const modal = document.getElementById('beta-feedback-modal'); const status = document.getElementById('screenshot-status'); // Hide modal so the screenshot shows the actual page if (modal) modal.style.visibility = 'hidden'; try { // Use Screen Capture API (browser shows picker while modal is hidden) const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: 'screen' } }); // Restore modal immediately after user picks a source if (modal) modal.style.visibility = ''; status.classList.remove('hidden'); status.textContent = 'Capturing...'; // Capture frame from stream const video = document.createElement('video'); video.srcObject = stream; await video.play(); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); // Stop stream stream.getTracks().forEach(track => track.stop()); // Convert to blob and upload status.textContent = 'Uploading screenshot...'; canvas.toBlob(async (blob) => { await uploadScreenshotBlob(blob); }, 'image/png'); } catch (err) { // Restore modal on error too if (modal) modal.style.visibility = ''; console.error('Screenshot capture failed:', err); status.classList.remove('hidden'); status.textContent = err.name === 'NotAllowedError' ? 'Screen capture was cancelled' : 'Failed to capture screenshot'; setTimeout(() => status.classList.add('hidden'), 3000); } }; window.uploadScreenshot = function(input) { if (!input.files || !input.files[0]) return; const file = input.files[0]; const status = document.getElementById('screenshot-status'); status.classList.remove('hidden'); status.textContent = 'Uploading...'; uploadScreenshotBlob(file); }; async function uploadScreenshotBlob(blob) { const status = document.getElementById('screenshot-status'); const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; const formData = new FormData(); formData.append('screenshot', blob, 'screenshot.png'); try { const response = await fetch('/api/beta/feedback/screenshot/', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }, body: formData }); const data = await response.json(); if (data.status === 'ok') { document.getElementById('feedback-screenshot-url').value = data.url; document.getElementById('screenshot-img').src = data.url; document.getElementById('screenshot-preview').classList.remove('hidden'); document.getElementById('screenshot-buttons').classList.add('hidden'); status.classList.add('hidden'); } else { status.textContent = data.message || 'Upload failed'; setTimeout(() => status.classList.add('hidden'), 3000); } } catch (err) { console.error('Screenshot upload failed:', err); status.textContent = 'Upload failed'; setTimeout(() => status.classList.add('hidden'), 3000); } } window.clearScreenshot = function() { document.getElementById('feedback-screenshot-url').value = ''; document.getElementById('screenshot-img').src = ''; document.getElementById('screenshot-preview').classList.add('hidden'); document.getElementById('screenshot-buttons').classList.remove('hidden'); }; })(); </script> <!-- Error Tracking for All --> <script> (function() { const loggedErrors = new Set(); // Dedupe within session const MAX_ERRORS = 5; // Max errors to log per page load let errorCount = 0; // Third-party scripts to ignore (ads, analytics, etc.) const IGNORED_PATTERNS = [ /venatus/i, /vntsm\.com/i, /googletag/i, /googletagmanager/i, /google-analytics/i, /doubleclick/i, /securepubads/i, /googlesyndication/i, /fundingchoicesmessages/i, /googlefcPresent/i, /adsystem/i, /adsbygoogle/i, /pagead/i, /prebid/i, /amazon-adsystem/i, /facebook\.net/i, /connect\.facebook/i, /twitter\.com\/widgets/i, /platform\.twitter/i, /disqus/i, /cdn\.segment/i, /hotjar/i, /clarity\.ms/i, /newrelic/i, /sentry/i, /Script error\.?$/i, // Generic cross-origin script error ]; function isIgnored(/* ...strings */) { for (var i = 0; i < arguments.length; i++) { var s = arguments[i]; if (s && IGNORED_PATTERNS.some(function(p) { return p.test(s); })) return true; } return false; } function logError(source, type, message, stack, extra) { // Limit errors per page to prevent flooding if (errorCount >= MAX_ERRORS) return; // Skip third-party/ad network errors — check message, stack, AND source filename var filename = extra && extra.filename || ''; if (isIgnored(message, stack, filename)) return; // Dedupe by fingerprint (type + first 100 chars of message) const fingerprint = type + ':' + (message || '').slice(0, 100); if (loggedErrors.has(fingerprint)) return; loggedErrors.add(fingerprint); errorCount++; fetch('/api/errors/log/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ source: source, type: type, message: message, stack: stack || '', url: window.location.href, userAgent: navigator.userAgent, ...extra }) }).catch(() => {}); // Silently fail } // Capture unhandled JS errors window.addEventListener('error', function(event) { var stack = event.error?.stack || ''; // Build a synthetic location string when the Error object has no stack // (common with cross-origin scripts or older browsers) if (!stack && event.filename) { stack = 'at ' + event.filename + ':' + (event.lineno || '?') + ':' + (event.colno || '?'); } logError( 'client', event.error?.name || 'Error', event.message, stack, { filename: event.filename || '', lineno: event.lineno || 0, colno: event.colno || 0 } ); }); // Capture unhandled promise rejections window.addEventListener('unhandledrejection', function(event) { var reason = event.reason; // Skip undefined/null rejections with no stack — almost always // third-party scripts (ad SDKs, consent managers) that reject // promises without providing a reason. Not actionable. if (reason == null && !(reason instanceof Error)) return; var message = reason?.message || String(reason); var stack = reason?.stack || ''; var type = 'UnhandledPromise'; // If reason is an Error subclass, use its name for better triage if (reason instanceof Error && reason.constructor?.name) { type = 'UnhandledPromise:' + reason.constructor.name; } // If reason isn't an Error, add type info for diagnostics if (reason && !(reason instanceof Error) && typeof reason !== 'string') { message = '[' + typeof reason + '] ' + message; } logError('client', type, message, stack); }); // Decorative fragments loaded lazily after render. Their failure is // invisible to the user, and 403/429 on them comes from the edge // (WAF/bot protection) or rate limiting rather than an app fault. var OPTIONAL_FRAGMENT_PATTERNS = [ /^\/forums\/topic\/\d+\/related\/$/, /^\/forums\/api\/popular-topics\/$/, /^\/forums\/api\/tab\//, ]; var EDGE_BLOCK_STATUSES = [403, 429]; function isOptionalFragmentBlock(path, status) { if (EDGE_BLOCK_STATUSES.indexOf(status) === -1) return false; return OPTIONAL_FRAGMENT_PATTERNS.some(function(p) { return p.test(path); }); } // Capture HTMX errors document.body.addEventListener('htmx:responseError', function(event) { var reqPath = event.detail.pathInfo?.requestPath || 'unknown'; if (isOptionalFragmentBlock(reqPath, event.detail.xhr.status)) return; logError('htmx', 'ResponseError', 'HTTP ' + event.detail.xhr.status + ' on ' + reqPath, '', { requestUrl: reqPath, triggerId: event.detail.elt?.id }); }); document.body.addEventListener('htmx:sendError', function(event) { var reqUrl = event.detail.pathInfo?.requestPath || event.detail.requestConfig?.path || event.detail.elt?.getAttribute('hx-get') || event.detail.elt?.getAttribute('hx-post') || 'unknown'; logError('htmx', 'NetworkError', 'Request failed', '', { requestUrl: reqUrl, triggerId: event.detail.elt?.id }); }); })(); </script> <!-- Syntax Highlighting for code blocks (site-wide) --> <link rel="stylesheet" id="hljs-theme-light" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> <link rel="stylesheet" id="hljs-theme-dark" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> <script> (function() { function syncHljsTheme() { var dark = document.documentElement.classList.contains('dark'); var light = document.getElementById('hljs-theme-light'); var darkEl = document.getElementById('hljs-theme-dark'); if (light) light.disabled = dark; if (darkEl) darkEl.disabled = !dark; } syncHljsTheme(); new MutationObserver(syncHljsTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); })(); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <!-- Game dev languages (C++, JS, Python, Java, JSON, XML, Bash, CSS are in core bundle) --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/glsl.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/csharp.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/lua.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/swift.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/kotlin.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/objectivec.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/cmake.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/wasm.min.js"></script> <script> document.addEventListener("DOMContentLoaded", function() { document.querySelectorAll('pre code').forEach((block) => { block.classList.remove('language-plaintext'); }); if (typeof hljs !== 'undefined') { hljs.highlightAll(); } }); </script> <script src="/static/gamedevnext/js/code-decorate.237c7d751ec8.js" defer></script> <!-- Global Toast Notification System --> <div id="toast-container" class="fixed bottom-16 right-4 z-50 flex flex-col gap-2 pointer-events-none"></div> <style> .toast { pointer-events: auto; transform: translateX(100%); opacity: 0; transition: transform 300ms ease-out, opacity 300ms ease-out; } .toast.show { transform: translateX(0); opacity: 1; } .toast.hiding { transform: translateX(100%); opacity: 0; } </style> <script> (function() { const toastColors = { 'success': 'bg-green-600 text-white', 'error': 'bg-red-600 text-white', 'warning': 'bg-amber-500 text-white', 'info': 'bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900', 'debug': 'bg-gray-600 text-white' }; const toastIcons = { 'success': '<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>', 'error': '<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>', 'warning': '<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>', 'info': '<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>' }; /** * Show a toast notification * @param {string} message - The message to display * @param {string} type - Type: 'success', 'error', 'warning', 'info' * @param {object} options - Optional settings: duration, action, actionLabel, isMessageNotification */ window.showToast = function(message, type = 'info', options = {}) { const container = document.getElementById('toast-container'); if (!container) return; // Suppress message notifications when on the messages page if (options.isMessageNotification && window.isMessagesPage) return; const duration = options.duration || (type === 'error' ? 0 : (type === 'warning' ? 6000 : 4000)); const action = options.action; const actionLabel = options.actionLabel || 'Undo'; const toast = document.createElement('div'); toast.className = `toast ${toastColors[type] || toastColors.info} px-4 py-3 rounded-lg shadow-lg text-sm flex items-center gap-3 max-w-sm`; let html = `${toastIcons[type] || toastIcons.info}<span class="flex-1">${message}</span>`; if (action) { html += `<button class="toast-action font-medium underline underline-offset-2 hover:no-underline">${actionLabel}</button>`; } html += `<button class="toast-close opacity-70 hover:opacity-100 transition-opacity" aria-label="Dismiss"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button>`; toast.innerHTML = html; container.appendChild(toast); // Trigger reflow for animation toast.offsetHeight; toast.classList.add('show'); const dismiss = () => { toast.classList.add('hiding'); toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }; // Close button toast.querySelector('.toast-close').addEventListener('click', dismiss); // Action button if (action) { toast.querySelector('.toast-action').addEventListener('click', () => { action(); dismiss(); }); } if (duration > 0) { setTimeout(dismiss, duration); } return { dismiss }; }; /** * Show a message notification toast with quick reply capability * @param {object} data - Message notification data * - senderName: Name of the sender * - senderAvatar: Avatar URL (optional) * - preview: Message preview text * - conversationId: Conversation ID for reply */ window.showMessageToast = function(data) { const container = document.getElementById('toast-container'); if (!container || window.isMessagesPage) return; const toast = document.createElement('div'); toast.className = 'toast bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-4 py-3 rounded-lg shadow-lg text-sm max-w-sm'; const avatarHtml = data.senderAvatar ? `<img src="${data.senderAvatar}" alt="${data.senderName}" class="w-10 h-10 rounded-full object-cover">` : `<div class="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 font-medium">${(data.senderName || '?').charAt(0).toUpperCase()}</div>`; toast.innerHTML = ` <div class="flex items-start gap-3 mb-2"> ${avatarHtml} <div class="flex-1 min-w-0"> <p class="font-medium text-gray-900 dark:text-white truncate">${data.senderName || 'Unknown'}</p> <p class="text-gray-600 dark:text-gray-400 text-xs truncate">${data.preview || 'New message'}</p> </div> <button class="toast-close text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" aria-label="Dismiss"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> </div> <form class="quick-reply-form flex gap-2"> <input type="text" class="flex-1 px-3 py-1.5 text-xs bg-gray-100 dark:bg-gray-700 border-0 rounded-lg focus:ring-2 focus:ring-sky-500 text-gray-900 dark:text-white placeholder-gray-400" placeholder="Quick reply..." autocomplete="off"> <button type="submit" class="px-3 py-1.5 bg-sky-600 text-white text-xs rounded-lg hover:bg-sky-700 disabled:opacity-50"> Send </button> </form> <div class="flex gap-2 mt-2"> <a href="/messages/${data.conversationId}/" class="text-xs text-sky-600 dark:text-sky-400 hover:underline">Open conversation</a> </div> `; container.appendChild(toast); toast.offsetHeight; toast.classList.add('show'); const dismiss = () => { toast.classList.add('hiding'); toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }; toast.querySelector('.toast-close').addEventListener('click', dismiss); // Handle quick reply form const form = toast.querySelector('.quick-reply-form'); const input = form.querySelector('input'); const submitBtn = form.querySelector('button[type="submit"]'); form.addEventListener('submit', async (e) => { e.preventDefault(); const content = input.value.trim(); if (!content) return; submitBtn.disabled = true; submitBtn.textContent = '...'; try { const formData = new FormData(); formData.append('content', content); const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1] || ''; const response = await fetch(`/messages/api/${data.conversationId}/send/`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, 'X-Requested-With': 'XMLHttpRequest' }, body: formData }); if (response.ok) { window.showToast('Reply sent!', 'success'); dismiss(); } else { const errData = await response.json(); window.showToast(errData.error || 'Failed to send', 'error'); submitBtn.disabled = false; submitBtn.textContent = 'Send'; } } catch (err) { window.showToast('Failed to send reply', 'error'); submitBtn.disabled = false; submitBtn.textContent = 'Send'; } }); // Auto-dismiss after 15 seconds (longer for message notifications) setTimeout(dismiss, 15000); return { dismiss }; }; // ===================================== // Global Emoji Conversion Utility // ===================================== window.emojiUtils = (function() { // Text emoticons → emoji const TEXT_EMOTICONS = { ':-)': '😊', ':)': '😊', '=)': '😊', ':-D': '😄', ':D': '😄', '=D': '😄', ':-P': '😛', ':P': '😛', ':p': '😛', ':-p': '😛', ';-)': '😉', ';)': '😉', ':-(': '😢', ':(': '😢', '=(': '😢', ":'(": '😭', ':-O': '😮', ':O': '😮', ':o': '😮', ':-o': '😮', 'O_O': '😳', 'o_o': '😳', ':-/': '😕', ':/': '😕', ':-\\': '😕', ':\\': '😕', ':-|': '😐', ':|': '😐', 'B-)': '😎', 'B)': '😎', '8-)': '😎', '8)': '😎', '<3': '❤️', '</3': '💔', ':-*': '😘', ':*': '😘', '^_^': '😊', '^-^': '😊', '>_<': '😣', '>.<': '😣', '-_-': '😑', 'XD': '😆', 'xD': '😆', ':3': '😺', ':-3': '😺', }; // Common shortcodes (subset for client-side) const SHORTCODES = { ':smile:': '😄', ':grin:': '😁', ':joy:': '😂', ':wink:': '😉', ':blush:': '😊', ':heart_eyes:': '😍', ':sunglasses:': '😎', ':thinking:': '🤔', ':cry:': '😢', ':sob:': '😭', ':thumbsup:': '👍', ':thumbs_up:': '👍', ':+1:': '👍', ':thumbsdown:': '👎', ':-1:': '👎', ':clap:': '👏', ':wave:': '👋', ':pray:': '🙏', ':muscle:': '💪', ':heart:': '❤️', ':fire:': '🔥', ':100:': '💯', ':star:': '⭐', ':sparkles:': '✨', ':zap:': '⚡', ':boom:': '💥', ':game:': '🎮', ':gamepad:': '🎮', ':trophy:': '🏆', ':crown:': '👑', ':gem:': '💎', ':rocket:': '🚀', ':sword:': '⚔️', ':computer:': '💻', ':bug:': '🐛', ':bulb:': '💡', ':gear:': '⚙️', ':check:': '✅', ':x:': '❌', ':warning:': '⚠️', ':eyes:': '👀', ':tada:': '🎉', ':party:': '🎉', ':coffee:': '☕', }; // Build regex patterns (sorted by length, longest first) const emoticonKeys = Object.keys(TEXT_EMOTICONS).sort((a, b) => b.length - a.length); const emoticonPattern = new RegExp( '(?<![a-zA-Z0-9/])(' + emoticonKeys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')(?![a-zA-Z0-9])', 'g' ); const shortcodeKeys = Object.keys(SHORTCODES).sort((a, b) => b.length - a.length); const shortcodePattern = new RegExp( '(' + shortcodeKeys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'g' ); return { // Convert text emoticons like :) to emoji convertEmoticons(text) { if (!text) return text; return text.replace(emoticonPattern, (match, p1) => TEXT_EMOTICONS[p1] || match); }, // Convert shortcodes like :smile: to emoji convertShortcodes(text) { if (!text || !text.includes(':')) return text; return text.replace(shortcodePattern, (match) => SHORTCODES[match] || match); }, // Convert all emoji representations convertAll(text) { if (!text) return text; text = this.convertEmoticons(text); text = this.convertShortcodes(text); return text; }, // Get all available emoticons for reference getEmoticons() { return { ...TEXT_EMOTICONS }; }, getShortcodes() { return { ...SHORTCODES }; }, }; })(); // HTMX Integration: Listen for server-triggered toasts via HX-Trigger header // Server can send: HX-Trigger: {"showToast": {"message": "Saved!", "type": "success"}} document.body.addEventListener('showToast', function(evt) { const detail = evt.detail || {}; window.showToast(detail.message || 'Action completed', detail.type || 'info', { duration: detail.duration, action: detail.action, actionLabel: detail.actionLabel }); }); // Also listen for htmx:afterRequest for HX-Trigger headers document.body.addEventListener('htmx:afterRequest', function(evt) { const trigger = evt.detail.xhr?.getResponseHeader('HX-Trigger'); if (trigger) { try { const parsed = JSON.parse(trigger); if (parsed.showToast) { const t = parsed.showToast; window.showToast(t.message, t.type, t); } } catch (e) { // Not JSON or doesn't have showToast } } }); // Render Django messages as toasts on page load document.addEventListener('DOMContentLoaded', function() { // Moderator queue notification }); })(); </script> <style> img.img-broken { position: relative; min-height: 40px; background: #f3f4f6; border-radius: 0.375rem; } .dark img.img-broken { background: #1f2937; } img.img-broken::after { content: attr(alt); display: flex; align-items: center; justify-content: center; position: absolute; inset: 0; background: inherit; color: #9ca3af; font-size: 0.75rem; border-radius: inherit; padding: 0.5rem; text-align: center; overflow: hidden; } </style> <script> // Global broken-image fallback — runs in two stages to avoid false positives: // 1) First error: strip the Referer (no-referrer) and/or upgrade to https, // then retry. Many external hosts (Google user-content, Imgur, etc.) // 403 with a Referer but load fine without one. // 2) Still failing: mark .img-broken so the CSS ::after overlay shows. // Also re-checks once the element actually loaded successfully and clears // the broken state if it recovers (e.g. slow network). document.addEventListener('error', function(e) { var img = e.target; if (!img || img.tagName !== 'IMG') return; // Skip images inside TipTap/ProseMirror editors — they have their own retry logic if (img.closest && img.closest('.ProseMirror, [contenteditable], .resizable-image-container')) return; if (!img.getAttribute('src')) return; if (!img.dataset.retryAttempted) { img.dataset.retryAttempted = 'true'; var src = img.getAttribute('src'); var changed = false; // Drop Referer on retry if (img.getAttribute('referrerpolicy') !== 'no-referrer') { img.setAttribute('referrerpolicy', 'no-referrer'); changed = true; } // Upgrade mixed-content http:// to https:// if (src.indexOf('http://') === 0) { src = 'https://' + src.slice(7); changed = true; } // Force a reload by resetting src (cache-busted) img.src = src + (src.indexOf('?') === -1 ? '?' : '&') + '_r=' + Date.now(); return; } if (img.dataset.fallbackApplied) return; img.dataset.fallbackApplied = 'true'; img.classList.add('img-broken'); if (!img.alt) img.alt = 'Image unavailable'; }, true); // If an image eventually loads after being marked broken, clear the overlay. document.addEventListener('load', function(e) { var img = e.target; if (!img || img.tagName !== 'IMG') return; if (img.naturalWidth > 0 && img.classList.contains('img-broken')) { img.classList.remove('img-broken'); } }, true); </script> <div data-nosnippet x-data="{ show: false, readConsent() { try { return localStorage.getItem('gdx_cookie_consent'); } catch (err) { return null; } }, writeConsent(value) { try { localStorage.setItem('gdx_cookie_consent', value); return true; } catch (err) { return false; } }, init() { var stored = this.readConsent(); if (!stored) { this.show = true; } else if (stored === 'accepted') { if (window.gtag) { gtag('consent', 'update', {'analytics_storage': 'granted'}); } } else if (stored === 'rejected') { this.show = false; } else { this.show = true; } }, accept() { var persisted = this.writeConsent('accepted'); this.show = false; if (!persisted) return; if (window.gtag) { gtag('consent', 'update', {'analytics_storage': 'granted'}); } window.dispatchEvent(new Event('gdx:analytics-consent-granted')); }, reject() { this.writeConsent('rejected'); this.show = false; if (window.gtag) { gtag('consent', 'update', {'analytics_storage': 'denied'}); } window.dispatchEvent(new Event('gdx:analytics-consent-denied')); } }" x-show="show" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="translate-y-full opacity-0" x-transition:enter-end="translate-y-0 opacity-100" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="translate-y-0 opacity-100" x-transition:leave-end="translate-y-full opacity-0" x-cloak class="fixed bottom-0 inset-x-0 z-[9999]" role="dialog" aria-label="Cookie consent"> <div class="mx-auto max-w-5xl px-4 pb-4"> <div class="flex flex-col sm:flex-row items-center justify-between gap-3 rounded-xl bg-gray-900 dark:bg-gray-800 border border-gray-700 px-5 py-4 shadow-2xl text-sm text-gray-300"> <p class="text-center sm:text-left"> With your permission, GameDev.net uses analytics cookies to understand how people use the platform. You can accept analytics or continue with necessary cookies only. <a href="/privacy/" class="underline text-blue-400 hover:text-blue-300 ml-1">Learn more</a> </p> <div class="flex shrink-0 items-center gap-2"> <button type="button" @click="reject()" class="rounded-lg border border-gray-600 bg-transparent hover:bg-gray-800 active:bg-gray-700 px-5 py-2 text-sm font-semibold text-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"> Necessary only </button> <button type="button" @click="accept()" class="rounded-lg bg-blue-600 hover:bg-blue-500 active:bg-blue-700 px-5 py-2 text-sm font-semibold text-white transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"> Accept analytics </button> </div> </div> </div> </div> </body> </html>