“Stay a while and Listen”

I added some visitor analytics to wordpress via a custom plugin

As a non-web programmer this was surprisingly easy…Only because I have AI.

Claude AI was able to throw everything into a script and threw it into a zip file and installed it into wordpress. A apart of me feels like I cheated but at the same time I don’t want to spend hours learning the wordpress database system, plugin api, and chart js just to see a few numbers and a line plot.

I’ve attached the code below that you can package into a .zip to upload to any wordpress site below.

Expand for plugin php code.
<?php
/**
 * Plugin Name: Visitor Counter Dashboard
 * Description: Displays visitor statistics in a graph on the admin dashboard
 * Version: 1.3.0
 * Author: Claude Ai (via Will Kolb)
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

class VisitorAnalytics {
    
    private $table_name;
    
    public function __construct() {
        global $wpdb;
        $this->table_name = $wpdb->prefix . 'visitor_analytics';
        
        // Hook into WordPress
        add_action('init', array($this, 'track_visitor'));
        add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
        add_action('wp_ajax_get_visitor_data', array($this, 'ajax_get_visitor_data'));
        
        // Activation hook
        register_activation_hook(__FILE__, array($this, 'create_table'));
    }
    
    /**
     * Create database table on plugin activation
     */
    public function create_table() {
        global $wpdb;
        
        $charset_collate = $wpdb->get_charset_collate();
        
        $sql = "CREATE TABLE {$this->table_name} (
            id mediumint(9) NOT NULL AUTO_INCREMENT,
            visit_date date NOT NULL,
            visit_count int(11) NOT NULL DEFAULT 1,
            ip_address varchar(45) NOT NULL,
            user_agent text,
            page_url varchar(255),
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY unique_daily_ip (visit_date, ip_address)
        ) $charset_collate;";
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }
    
    /**
     * Track visitor on each page load
     */
    public function track_visitor() {
        // Don't track admin users or admin pages
        if (is_admin() || current_user_can('manage_options')) {
            return;
        }
        
        global $wpdb;
        
        $ip_address = $this->get_user_ip();
        $today = current_time('Y-m-d');
        $user_agent = sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? '');
        $page_url = sanitize_text_field($_SERVER['REQUEST_URI'] ?? '');
        
        // Check if this IP has already been recorded today
        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT id FROM {$this->table_name} WHERE visit_date = %s AND ip_address = %s",
            $today, $ip_address
        ));
        
        if (!$existing) {
            // Insert new visitor record
            $wpdb->insert(
                $this->table_name,
                array(
                    'visit_date' => $today,
                    'ip_address' => $ip_address,
                    'user_agent' => $user_agent,
                    'page_url' => $page_url,
                    'visit_count' => 1
                ),
                array('%s', '%s', '%s', '%s', '%d')
            );
        }
    }
    
    /**
     * Get user's IP address
     */
    private function get_user_ip() {
        $ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
        
        foreach ($ip_keys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                $ip = sanitize_text_field($_SERVER[$key]);
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $ip;
                }
            }
        }
        
        return sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1');
    }
    
    /**
     * Add dashboard widget
     */
    public function add_dashboard_widget() {
        wp_add_dashboard_widget(
            'visitor_analytics_widget',
            'Visitor Analytics',
            array($this, 'display_dashboard_widget')
        );
    }
    
    /**
     * Display the dashboard widget content
     */
    public function display_dashboard_widget() {
        ?>
        <div id="visitor-analytics-container">
            <div style="margin-bottom: 15px;">
                <select id="analytics-period" style="margin-right: 10px;">
                    <option value="7">Last 7 days</option>
                    <option value="30">Last 30 days</option>
                    <option value="90">Last 90 days</option>
                </select>
                <button id="refresh-analytics" class="button button-secondary">Refresh</button>
            </div>
            
            <div id="analytics-summary" style="display: flex; gap: 20px; margin-bottom: 20px;">
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">Today</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #0073aa;" id="today-visitors">-</p>
                </div>
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">This Week</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #00a32a;" id="week-visitors">-</p>
                </div>
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">This Month</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #d63638;" id="month-visitors">-</p>
                </div>
            </div>
            
            <canvas id="visitor-chart" width="400" height="200" style="max-height: 200px;"></canvas>
            <div id="analytics-loading" style="text-align: center; padding: 20px;">Loading...</div>
        </div>
        
        <script>
        jQuery(document).ready(function($) {
            let chart = null;
            
            // Cleanup function to properly destroy chart
            function destroyChart() {
                if (chart && typeof chart.destroy === 'function') {
                    chart.destroy();
                    chart = null;
                }
            }
            
            // Clean up when leaving the page
            $(window).on('beforeunload', destroyChart);
            
            function loadAnalytics() {
                const period = $('#analytics-period').val();
                $('#analytics-loading').show();
                
                $.ajax({
                    url: ajaxurl,
                    type: 'POST',
                    data: {
                        action: 'get_visitor_data',
                        period: period,
                        nonce: '<?php echo wp_create_nonce('visitor_analytics_nonce'); ?>'
                    },
                    success: function(response) {
                        if (response.success) {
                            updateSummary(response.data.summary);
                            updateChart(response.data.chart_data);
                        }
                        $('#analytics-loading').hide();
                    },
                    error: function() {
                        $('#analytics-loading').hide();
                        alert('Error loading analytics data');
                    }
                });
            }
            
            function updateSummary(summary) {
                $('#today-visitors').text(summary.today || 0);
                $('#week-visitors').text(summary.week || 0);
                $('#month-visitors').text(summary.month || 0);
            }
            
            function updateChart(chartData) {
                const canvas = document.getElementById('visitor-chart');
                const ctx = canvas.getContext('2d');
                
                // Properly destroy existing chart
                if (chart) {
                    chart.destroy();
                    chart = null;
                }
                
                // Clear the canvas
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // Reset canvas size
                canvas.style.width = '100%';
                canvas.style.height = '200px';
                
                chart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: chartData.labels,
                        datasets: [{
                            label: 'Daily Visitors',
                            data: chartData.data,
                            borderColor: '#0073aa',
                            backgroundColor: 'rgba(0, 115, 170, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        interaction: {
                            intersect: false,
                            mode: 'index'
                        },
                        scales: {
                            y: {
                                beginAtZero: true,
                                ticks: {
                                    stepSize: 1,
                                    precision: 0
                                }
                            },
                            x: {
                                grid: {
                                    display: false
                                }
                            }
                        },
                        plugins: {
                            legend: {
                                display: false
                            },
                            tooltip: {
                                backgroundColor: 'rgba(0, 0, 0, 0.8)',
                                titleColor: '#fff',
                                bodyColor: '#fff',
                                cornerRadius: 4
                            }
                        },
                        elements: {
                            point: {
                                radius: 3,
                                hoverRadius: 6
                            }
                        }
                    }
                });
            }
            
            // Event listeners
            $('#analytics-period').change(loadAnalytics);
            $('#refresh-analytics').click(loadAnalytics);
            
            // Initial load
            loadAnalytics();
        });
        </script>
        <?php
    }
    
    /**
     * Enqueue admin scripts
     */
    public function enqueue_admin_scripts($hook) {
        if ($hook === 'index.php') {
            wp_enqueue_script('chart-js', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js', array(), '3.9.1', true);
        }
    }
    
    /**
     * AJAX handler for getting visitor data
     */
    public function ajax_get_visitor_data() {
        // Verify nonce
        if (!wp_verify_nonce($_POST['nonce'], 'visitor_analytics_nonce')) {
            wp_die('Security check failed');
        }
        
        // Check user permissions
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions');
        }
        
        global $wpdb;
        $period = intval($_POST['period']);
        
        // Get summary data
        $today = current_time('Y-m-d');
        $week_ago = date('Y-m-d', strtotime('-7 days', current_time('timestamp')));
        $month_ago = date('Y-m-d', strtotime('-30 days', current_time('timestamp')));
        
        $today_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date = %s", $today
        ));
        
        $week_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $week_ago
        ));
        
        $month_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $month_ago
        ));
        
        // Get chart data
        $start_date = date('Y-m-d', strtotime("-{$period} days", current_time('timestamp')));
        
        $chart_data = $wpdb->get_results($wpdb->prepare(
            "SELECT visit_date, COUNT(*) as visitor_count 
             FROM {$this->table_name} 
             WHERE visit_date >= %s 
             GROUP BY visit_date 
             ORDER BY visit_date ASC",
            $start_date
        ));
        
        // Fill in missing dates with zero counts
        $labels = array();
        $data = array();
        
        for ($i = $period - 1; $i >= 0; $i--) {
            $date = date('Y-m-d', strtotime("-{$i} days", current_time('timestamp')));
            $labels[] = date('M j', strtotime($date));
            
            $count = 0;
            foreach ($chart_data as $row) {
                if ($row->visit_date === $date) {
                    $count = intval($row->visitor_count);
                    break;
                }
            }
            $data[] = $count;
        }
        
        wp_send_json_success(array(
            'summary' => array(
                'today' => intval($today_count),
                'week' => intval($week_count),
                'month' => intval($month_count)
            ),
            'chart_data' => array(
                'labels' => $labels,
                'data' => $data
            )
        ));
    }
}

// Initialize the plugin
new VisitorAnalytics();

In gamedev news I want to make a “boss” area thing that the player has to destroy

I think I want to incorporate that helix thingy as the “power core” (see: this post).

I’ve made a few iterations but they keep looking like the Red power ranger’s vape pen:

My hope was to make an equivalent to the Doom 2016 gore nest or the half life 2 energy pylon things, so that the player would essentially break the core causing some gameplay rush event where you gotta kill a bunch of bots and run.

From https://half-life.fandom.com/wiki/Combine_Power_Generator

I might go with some kind of oppressive robotic face instead? Like the Mussolini face from WWII.

from https://en.wikipedia.org/wiki/Palazzo_Braschi

Loop-dee-Loop and Todo-s

I wanted to mesh paint (https://dev.epicgames.com/documentation/en-us/unreal-engine/getting-started-with-mesh-texture-color-painting-in-unreal-engine) a bit in unreal and thought “Oh I’ll just flip this virutal texture box”

I have a pretty good pc (4090 24gb) I’m surprised its taking so 5+ minutes (but then again I’ve made NO effort to optimize any of my materials…

That being said…

I guess I made things low poly enough that maybe this is inherently optimized….

But thats a detail thing that I can work on later, For the outside I want to:

  1. Make those back blocks look like a office building embedded into the rock
  2. Replace those generators with something that would flow better with game play
  3. Make a new texture for that platform. Tat texture is fine but I’m using it in like 12 places and I don’t want to mess with UV scaling just to get it working
  4. Make some static rock models to place along the edges of mountainous regions so I it doesn’t look like a muddy ski slope
  5. Do something better with these conveyor belts. I put in WAAY to much time to make those kinda work, I might just delete them
  6. I have this cabinet which is kinda sketchy looking, it was supposed to go with a terminal but I might move it again
  7. Fix that lighting so that there isn’t a weird skylight for a warehouse setting
  8. Get some friends for that tree.

Also I need to make a better road texture, at far distances it looks fine but then you zoom in…

Every-time I look at it my graphics card fan spins up…probably a bad thing.

I also added a tunnel model

The UV’s and normals are REALLY messed up atm but I dont want to dig in yet. I fixed it by making the underlying material double sided but generally if you’re a single sided material and your model goes from looking like this in blender:

To this…

You should probably do it again….

But in the meantime there’s the magic “Make the material two sided” button which is normally used for meshes that have both an interior and exterior (which is not this….at all)

Still has issues. The darkness underneath comes from the unreal seeing that the normal of the face is facing in a direction that is into the block’s origin rather than away from it. This makes the lighting calculations get all wonky.

I’ve been disorganized when working this (also I stopped working it for like 2 weeks and now I’m lost) So I made a trello board here:
https://trello.com/b/dmIooAod/blacklaceworking-board

Broken into:
“Longer term goals”- Things I probably wont look into until I have something on steam

“MVP Make/Design”- Things I want to add to the game before I submit a demo/game to steamworks

“Fix/Improve” – Things that are in the game but need to be re-worked.

I’ll start chipping away at these and my personal goal is to get something ready to throw on steam by June.

Maybe a Car?

I started making a car

I was trying to base it off of https://www.classic.com/m/bmw/3-series/e36/m3/ and I think the vibe is kinda there? I think the wheel wells are kinda in the right place:

My goal is to have a driving sequence at some point. But honestly this might just be a thing I hold off on attempting until I finish up the whole “game” part of the game…Plus the doors seem like they would be hard to get right.

They Keep killing each other…

I re-wrote a bunch of the patrol bot AI to use EQS (Environmental query system: see https://dev.epicgames.com/documentation/en-us/unreal-engine/environment-query-system-quick-start-in-unreal-engine#2-environmentquerycontext ) , which should greatly simplify the the bots and give them better criteria to find spots that see the player, but are also kinda close.

Here’s the EQS Tree:

Essentially EQS is a way to break up the navigation grid into nodes, each has a score and the EQS tree determines the score of each node. (Lower the better). In this case I have two tests for each node: Can the node see the current location of the player? Am I close to the player? Here’s another video of things kinda working but not fully:

I also fixed up the rocket so now it wont fly through walls

Here’s some quick gameplay of what I have atm:

It’s very silent right now…Also the stabby bots are quick as hell and I want some kinda warning for that. The gameplay right now is very serious sam and I want to tone it back speed wise. I think I might just slow down all the characters and add it some head bob.

Longer gameplay (with sound) below.

The map I was playing on isn’t great but I figure it might be better than that dev arena that I had before. I’m in the process of re-making the warehouse as a big static mesh so it’s easier on lower performance machines (right now it’s like 200+ meshes).

My 4090 fan is starting to spin up more often so I gotta feeling I might be hitting the limit of my “optimize later” strategy.

Build build build build

I made a real spawn location for the patrol bots:

Isn’t too much to it, just a bunch of boxes and a conveyor that spits out the bots.

I reworked some of the deployment logic so that bots are physics objects before they get fully deployed. That way I can do stuff like drop them off cliffs after being built.

The deployment logic now hardcodes the mesh’s position and rotation before playing the deploy animation (which is why you see them jump up in the second video). A way I can fix/clean this up is by adding a rollover animation which will play if the mesh is disoriented.

Right now the assemblers are attached to timers, when I get this hooked up to the survival game mode I’ll add control back to the game mode to dump out bots as needed.

I also added this guy in game

Doesn’t look great but I want to have one of these at the side of each assembler and allow some kind of “override” command or mini-game to stop the flow of bots or to flip them to your side.

Rockets…man

Gameplay isn’t done yet but I got some paint setup on the rocket launcher and the rocket.

Threw a bunch of random phrases+numbers on it, still looks pretty cartoon-y (but so does everything atm).

Next step is to get the firing + reload sequence setup.

Misc.

Added a parameter to scale the grenade explosion radius so you kinda see where you’ll get hit:

Its setup via user parameters and HLSL which I havent used in a while (I was fluent in the xna days, but that’s pushing 10+ years ago now)

I might dig into this more just to get my feet wet again with custom shader code. Here’s the blueprint setup I had to do in order to get this working properly

Nothing crazy but I always treat actors as these sacred classes that I want to minimize. Therefore when I wanted the particle system to stay put after the grenade actor destroyed itself I started going down a detachment rabbit hole. But after some googling I realized I should stop being scared of spamming actors for whatever I need. In this case I made an actor that is JUST the grenade explosion holder.

I also started modeling this guy:

Which I wanted to make a “large” version of the patrol bot that has rockets on it, then I wanted to get a laser setup coming out the front. I’m moving towards the idea that each patrol bot has to reload after ANY burst, otherwise the game probably will be way to hard.

Probably will push more on this friday/saturday/sunday to get the new bot in place. I also want to retool the flashbang grenade bots to hold the flashbang in its “hand” as it goes off and reload. No real reason other than I think that will be sillier and give a distinction to the frag grenade bots.

Still need to make more maps, still need to make a higher level meta game, still need to add more audio. Uhhhggg I probably should make a trello board…

UE Control rigs and Custom Numpad setup

I kept looking at the last video I posted and got really annoyed at how bad the animations of the patrol bot looked. This drove me to remake the animation rig for the patrol bot (to be much, much simpler) and I started making a control rig in unreal to push the animations there (see https://dev.epicgames.com/documentation/en-us/unreal-engine/how-to-create-control-rigs-in-unreal-engine).

Now why use Control rigs? Honestly not too sure yet… My hope is that IK is easier (so I dont have to add IK bones) but adding these controls seems tedious, in addition your forward solve block I guess has to be a big tower of blueprint power?

I’m probably missing the plot here, but also my “forward solve” seems to be undoing my “backwards solve” where I assumed “backwards solve” was used for IK situations. I’m still playing around but the hope is that by using unreal internals I should be able to handle the physicality of the bots a bit better than I expect.

Also I had an gmmk numpad (https://www.gloriousgaming.com/products/gmmk-numpad-keyboard?srsltid=AfmBOoqt9KEojE6tva-cmlDcTDtw1XiBNFEktoFWoobeNvWKYGD8ZtL0 I got it on sale for $90 but I think its still crazy overpriced) which I wanted to use for ableton and never got working (the slider plus the knob weren’t configurable to midi in). So I installed a via flavor of QMK ( https://caniusevia.com/ ) which lets me configure my keyboard in browser.

Which would be amazing if: 1.) Firefox supported usbhid and 2.) If I could remap the slider. But right now its really good for me using OBS to record rather than the snipping tool which records at like 20fps.

So for example here’s the control rig I described in action, BUT there’s no OBS window visible!

Otherwise I think I’m still dead-set on remaking the patrol bot animations in unreal. Walking and reload might be the most annoying but mostly I want to be able to make quick animations without the huge headache of .fbx exporting and importing (even with the blender unreal addons https://www.unrealengine.com/en-US/blog/download-our-new-blender-addons , which work amazing for static meshes, kinda sketchy for skeletal). I kinda wish unreal had the option of slicing the animation list of an FBX and attempting bone resolution before importing. I really want to get this working because then my workflow stays in unreal for animating. Blender is still blows unreal out of the water for making meshes IMO but animations in Blender still seem hacky with the action editor.

There are a few things I’m actively annoyed with when it comes to control rigs (which I wont show you here because I’m still WIP with this one.) I’m also a straight up control rig novice so I bet as I learn these problems might be solved with better practices.

1.) You cant manipulate the control shapes in the editor preview window. Seems like that would be an easy addition and should match the same kind of workflow as the “hold control to bump physics asset” thing.

2.) Control rigs are effected by bones. This one I get WHY but it seems counter-intuitive that you would ever want to make a rig that is controlled by a parent bone. I get the idea of attaching to a bone in order to have like a small sub object (for example a turret on a large ship).

3.) When you add a control to a bone it adds it to the children of that Bone. This would be fine if #2 wasn’t a thing.

4.) Adding Default forward solve assignments are not automated. I bet I could find a python script to do this but still, that blueprint tower of power really can and should be made upon generating a new control for a bone.

Still gonna push ahead with the control rigs though…Modular rigs freak me out (https://dev.epicgames.com/community/learning/tutorials/Dd31/unreal-engine-modular-control-rig-rigging-with-modules) and seem to be used primarily for bipedal skeletons.

Some Texture Painting / Raspberry Pi fun

Attempted to add some color to the zapper I showed earlier. I’m not 100% enthusiastic about the job I did but I still like some of the ideas I have here.

For the record this is how you setup a shader in blender for texture Paint:

Essentially you make a shader that you like as your “base”, then make an image that is zero alpha. Then you tie the alpha of the texture into a color mix node, that way when you paint on the texture it will swap in the information on the image to the shader. If I wanted to get REALLY creative here I would add in something like a chipping algorithm based upon the tangent of the base model so you would get a “worn” look to everything.

The potato cannon button I think came out fine, the wires could have used a bit more slack (or maybe some stables holding it down).

The front of the cannon I tried adding some scorching but honestly I botched that portion so it looks more like someone dipped the front in soot and smeared it back.

The bat handle is a bit too cartoony. The wrap needed to be tighter but I already applied the screw modifier onto the object so I was stuck with this. I’ll probably remake this if I keep the same idea.

The shoulder brace bike tire I’m weirdly happy with (minus the un-beveled edges), making a tire is surprisingly difficult in blender (for me atleast).

The pylons in the back I think look kinda cool, but they seem crazy out of place to the rest of the weapon. They don’t have that “junkyard” kinda look I was going for (also without lighting its hard to see the green emissions)

The can and the junction box are fine, I’d want to add a label to the can and some screws to the junction box.

In other news I spent 3 hours debugging my asus bt500 (https://www.asus.com/us/networking-iot-servers/adapters/all-series/usb-bt500/) on a raspberry pi 5 so I could get my xbox controller hooked up to run steamlink (which recently was released for the raspberry pi 5 arch https://help.steampowered.com/en/faqs/view/6424-467A-31D9-C6CB). I’m using this kit https://www.amazon.com/CanaKit-Raspberry-Starter-Kit-PRO/dp/B0CRSNCJ6Y/ref=asc_df_B0CRSNCJ6Y?mcid=499475e052c83be5a802a944f85cf088&tag=hyprod-20&linkCode=df0&hvadid=693601922380&hvpos=&hvnetw=g&hvrand=8182359702763456621&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9002000&hvtargid=pla-2281722246870&psc=1 which I got on sale at microcenter. My thought was getting a fan would be better for long sessions of video decoding on steamlink.

I only wanted a bluetooth adapter because my xbox controller would have crazy delay to the raspberry pi 5 integrated bluetooth adapter, I only bought a bt500 because it was at microcenter and it was kinda cheap. Turns out the realtek chip inside of the bt500 isn’t natively supported by raspian (or linux really). After debugging for like 3-4 hours, I had a thought that maybe the cana kit fan was blocking the bluetooth signal, so I removed the fan tried the native bluetooth on the raspberry pi and everything worked perfectly.

tl;dr : I spent extra money to give myself more problems