diff options
-rwxr-xr-x | awk/forth/f.awk | 593 | ||||
-rw-r--r-- | awk/forth/test.forth | 157 | ||||
-rw-r--r-- | html/isometric-bounce/index.html (renamed from html/voxels/index.html) | 6 | ||||
-rw-r--r-- | html/isometric-bounce/js/game.js | 335 | ||||
-rw-r--r-- | html/voxels/js/game.js | 345 |
5 files changed, 1088 insertions, 348 deletions
diff --git a/awk/forth/f.awk b/awk/forth/f.awk new file mode 100755 index 0000000..23bc9f0 --- /dev/null +++ b/awk/forth/f.awk @@ -0,0 +1,593 @@ +#!/usr/bin/awk -f + +# Forth interpreter in AWK + +BEGIN { + print "Welcome to the AWK Forth Interpreter!" + print "Type your commands below. Use 'bye' to quit." + # Initialize variables + top = -1 # Initialize stack pointer + + # Initialize the dictionary with basic words + words["+"] = "+" + words["-"] = "-" + words["*"] = "*" + words["/"] = "/" + words["dup"] = "dup" + words["over"] = "over" + words["swap"] = "swap" + words["."] = "." + words["bye"] = "bye" + words["rot"] = "rot" + words["drop"] = "drop" + words["nip"] = "nip" + words["tuck"] = "tuck" + words["roll"] = "roll" + words["pick"] = "pick" + words["negate"] = "negate" + words["abs"] = "abs" + words["max"] = "max" + words["min"] = "min" + words["mod"] = "mod" + words["="] = "=" + words["see"] = "see" + words["if"] = "if" + words["then"] = "then" + words["else"] = "else" + words[">"] = ">" + + # Add descriptions for words + desc["+"] = "( n1 n2 -- sum ) Add top two numbers" + desc["-"] = "( n1 n2 -- diff ) Subtract top number from second" + desc["*"] = "( n1 n2 -- prod ) Multiply top two numbers" + desc["/"] = "( n1 n2 -- quot ) Divide second by top" + desc["dup"] = "( n -- n n ) Duplicate top of stack" + desc["over"] = "( n1 n2 -- n1 n2 n1 ) Copy second item to top" + desc["swap"] = "( n1 n2 -- n2 n1 ) Swap top two items" + desc["rot"] = "( n1 n2 n3 -- n2 n3 n1 ) Rotate top three items" + desc["drop"] = "( n -- ) Discard top item" + desc["nip"] = "( n1 n2 -- n2 ) Remove second item" + desc["tuck"] = "( n1 n2 -- n2 n1 n2 ) Copy top item below second" + desc["roll"] = "( nk ... n1 n0 k -- nk-1 ... n1 n0 nk ) Move kth item to top" + desc["pick"] = "( nk ... n1 n0 k -- nk ... n1 n0 nk ) Copy kth item to top" + desc["negate"] = "( n -- -n ) Negate number" + desc["abs"] = "( n -- |n| ) Absolute value" + desc["max"] = "( n1 n2 -- max ) Maximum of top two numbers" + desc["min"] = "( n1 n2 -- min ) Minimum of top two numbers" + desc["mod"] = "( n1 n2 -- rem ) Remainder of n1/n2" + desc["="] = "( n1 n2 -- flag ) Test if equal, leaves 1 if true, 0 if false" + desc["if"] = "( flag -- ) Begin conditional execution" + desc["then"] = "( -- ) End conditional execution" + desc["else"] = "( -- ) Execute if previous condition was false" + desc[">"] = "( n1 n2 -- flag ) Returns true if n1 is greater than n2" + + # Add variable-related words to dictionary + words["variable"] = "variable" + words["!"] = "!" + words["@"] = "@" + words["?"] = "?" + + # Add descriptions for the new words + desc["variable"] = "( -- addr ) Create a variable" + desc["!"] = "( n addr -- ) Store n at addr" + desc["@"] = "( addr -- n ) Fetch contents of addr" + desc["?"] = "( addr -- ) Print contents of addr" + + # Initialize test-related words + words["test"] = "test" + words["testing"] = "testing" + desc["test"] = "( n1 n2 -- ) Compare values and report test result" + desc["testing"] = "( -- ) Print test description" + + # Initialize condition stack + cond_top = -1 + + # Mark these as compile-only words + compile_only["if"] = 1 + compile_only["then"] = 1 + compile_only["else"] = 1 + + # Add dot-quote to dictionary + words[".\""] = ".\"" + desc[".\""] = "( -- ) Print following string up to closing quote" + + # Handle file input + if (ARGC > 1) { + print "Loading file:", ARGV[1] + while ((getline < ARGV[1]) > 0) { + if ($0 ~ /^[[:space:]]*$/) continue # Skip empty lines + print "> " $0 + process_line() + } + close(ARGV[1]) + exit + } + + # Add to dictionary initialization + words["<"] = "<" + words[">"] = ">" + + # Add descriptions + desc["<"] = "( n1 n2 -- flag ) Returns true if n1 is less than n2" + desc[">"] = "( n1 n2 -- flag ) Returns true if n1 is greater than n2" + + # Add handlers + handlers["+" ] = "add" + handlers["-" ] = "subtract" + handlers["*" ] = "multiply" + handlers["/" ] = "divide" + handlers["dup"] = "dup" + handlers["over"] = "over" + handlers["swap"] = "swap" + handlers["."] = "print_top" + handlers["<"] = "less_than" + handlers[">"] = "greater_than" + + # Add variable handler + handlers["variable"] = "create_variable" + handlers["!"] = "store" + handlers["@"] = "fetch" + handlers["?"] = "print_var" +} + +# Add these helper functions near the top +function check_stack(min_items, error_msg) { + if (top < min_items - 1) { + print error_msg ? error_msg : "Error: Not enough values on stack" + return 0 + } + return 1 +} + +function check_underflow() { + return check_stack(1, "Error: Stack underflow") +} + +# Function to define a new word +function define_word(name, definition) { + # Store the raw definition + raw_definitions[name] = definition + print "DEBUG: Raw definition for " name ": " definition + + # Process the definition to handle conditionals + processed = process_definition(definition) + print "DEBUG: Processed definition for " name ": " processed + words[name] = processed +} + +function process_definition(definition) { + # Normalize whitespace in definition + gsub(/^[[:space:]]+/, "", definition) + gsub(/[[:space:]]+$/, "", definition) + gsub(/[[:space:]]+/, " ", definition) + + # Split definition into words + split(definition, def_words, " ") + processed = "" + in_string = 0 + string_content = "" + + for (i = 1; i <= length(def_words); i++) { + word = def_words[i] + + if (word == ".\"") { + # Start collecting string + in_string = 1 + string_content = "" + continue + } + + if (in_string) { + # Check if this word contains the closing quote + if (word ~ /"$/) { + # End of string found + string_content = string_content (string_content == "" ? "" : " ") substr(word, 1, length(word)-1) + processed = processed " <string>" string_content "</string>" + in_string = 0 + } else { + # Add to string content + string_content = string_content (string_content == "" ? "" : " ") word + } + continue + } + + # Handle non-string words + if (word == "if") { + processed = processed " <if>" + } else if (word == "else") { + processed = processed " <else>" + } else if (word == "then") { + processed = processed " <then>" + } else { + processed = processed " " word + } + } + + return processed +} + +# Stack to hold values +function push(value) { + stack[++top] = value +} + +function pop() { + if (top < 0) { + print "Error: Stack underflow" + return 0 + } + return stack[top--] +} + +function binary_op(operation) { + if (!check_stack(2)) return + second = pop() + first = pop() + if (operation == "+") push(first + second) + else if (operation == "-") push(first - second) + else if (operation == "*") push(first * second) + else if (operation == "/") { + if (second == 0) { + print "Error: Division by zero" + push(first) + push(second) + return + } + push(first / second) + } + else if (operation == "mod") push(first % second) + else if (operation == "=") push(first == second ? 1 : 0) + else if (operation == "<") push(first < second ? 1 : 0) + else if (operation == ">") push(first > second ? 1 : 0) +} + +# Then simplify the operation functions: +function add() { binary_op("+") } +function subtract() { binary_op("-") } +function multiply() { binary_op("*") } +function divide() { binary_op("/") } +function mod() { binary_op("mod") } +function equals() { binary_op("=") } +function less_than() { binary_op("<") } +function greater_than() { binary_op(">") } + +# Function to duplicate the top value +function dup() { + if (!check_stack(1)) return + push(stack[top]) +} + +# Function to copy the second value to the top +function over() { + if (!check_stack(2)) return + push(stack[top - 1]) +} + +# Function to swap the top two values +function swap() { + if (!check_stack(2)) return + temp = pop() + second = pop() + push(temp) + push(second) +} + +function print_top() { + if (!check_stack(1)) return + print stack[top] +} + +# Function to list all available words +function list_words() { + print "Available words:" + for (word in words) { + print word + } +} + +function rot() { + if (!check_stack(3)) return + third = pop() + second = pop() + first = pop() + push(second) + push(third) + push(first) +} + +function drop() { + if (!check_stack(1)) return + top-- +} + +function nip() { + if (!check_stack(2)) return + temp = stack[top] + drop() + drop() + push(temp) +} + +function tuck() { + if (!check_stack(2)) return + temp = pop() + second = pop() + push(temp) + push(second) + push(temp) +} + +function roll() { + if (!check_stack(1)) return + n = int(pop()) + if (!check_stack(n)) return + if (n <= 0) return + + temp = stack[top - n + 1] + for (i = top - n + 1; i < top; i++) { + stack[i] = stack[i + 1] + } + stack[top] = temp +} + +function pick() { + if (!check_stack(1)) return + n = int(pop()) + if (!check_stack(n)) return + if (n < 0) return + push(stack[top - n]) +} + +function negate() { + if (!check_stack(1)) return + push(-pop()) +} + +function abs() { + if (!check_stack(1)) return + n = pop() + push(n < 0 ? -n : n) +} + +function max() { + if (!check_stack(2)) return + b = pop() + a = pop() + push(a > b ? a : b) +} + +function min() { + if (!check_stack(2)) return + b = pop() + a = pop() + push(a < b ? a : b) +} + +function see(word) { + if (word in desc) { + print desc[word] + } else if (word in words) { + print ": " word " " words[word] " ;" + } else { + print "Word '" word "' not found" + } +} + +# Add these new functions for variable handling: + +# Function to create a new variable +function create_variable(name) { + if (name == "") { + print "Error: Variable name required" + return + } + variables[name] = 0 # Initialize variable to 0 + # No need to push the variable name onto the stack +} + +# Function to store a value in a variable +function check_variable(addr, error_msg) { + if (!(addr in variables)) { + print error_msg ? error_msg : "Error: Invalid variable '" addr "'" + return 0 + } + return 1 +} + +function store() { + if (!check_stack(2)) return + addr = pop() + if (!check_variable(addr)) return + variables[addr] = pop() +} + +# Function to fetch a value from a variable +function fetch() { + if (!check_stack(1)) return + addr = pop() + if (!check_variable(addr)) return + push(variables[addr]) +} + +# Function to print a variable's value +function print_var() { + if (top < 0) { + print "Error: Stack underflow" + return + } + addr = pop() + if (!(addr in variables)) { + print "Error: Invalid variable '" addr "'" + return + } + print variables[addr] +} + +# Add these test-related functions: +function run_test(expected) { + if (top < 0) { + print "❌ FAIL: Stack is empty" + return + } + actual = pop() + if (actual == expected) { + print "✓ PASS" + } else { + print "❌ FAIL: Expected", expected, "but got", actual + } +} + +function print_test_description() { + if (NF < 2) { + print "Error: testing requires a description" + return + } + # Collect all words after 'testing' as the description + test_desc = "" + for (i = 2; i <= NF; i++) { + test_desc = test_desc " " $i + } + print "\nTesting:" test_desc +} + +# Add these new functions for conditional handling: + +# Function to handle if statement +function handle_if() { + if (top < 0) { + print "Error: Stack underflow" + return + } + # Save condition without consuming it + cond_stack[++cond_top] = (stack[top] != 0) +} + +# Function to handle then statement +function handle_then() { + if (cond_top < 0) { + print "Error: Unmatched then" + return + } + cond_top-- # Pop condition +} + +# Function to handle else statement +function handle_else() { + if (cond_top < 0) { + print "Error: Unmatched else" + return + } + cond_stack[cond_top] = !cond_stack[cond_top] +} + +# Add this function +function execute_word(word) { + if (word in handlers) { + if (handlers[word] == "add") add() + else if (handlers[word] == "subtract") subtract() + else if (handlers[word] == "multiply") multiply() + else if (handlers[word] == "divide") divide() + else if (handlers[word] == "dup") dup() + else if (handlers[word] == "over") over() + else if (handlers[word] == "swap") swap() + else if (handlers[word] == "print_top") print_top() + else if (handlers[word] == "create_variable") { + # Special handling for variable creation + if (i + 1 <= NF) { + create_variable($(++i)) + } else { + print "Error: variable requires a name" + } + } + else if (handlers[word] == "store") store() + else if (handlers[word] == "fetch") fetch() + else if (handlers[word] == "print_var") print_var() + else { + print "Error: Handler '" handlers[word] "' not implemented" + return 0 + } + return 1 + } + return 0 +} + +# Move the main command processing into a function +function process_line() { + # Process input only if it's not empty + if (NF > 0) { + # Remove comments and normalize whitespace + gsub(/\(.*\)/, "") # Remove everything from ( to ) + gsub(/^[[:space:]]+/, "") # Remove leading whitespace + gsub(/[[:space:]]+$/, "") # Remove trailing whitespace + gsub(/[[:space:]]+/, " ") # Normalize internal whitespace + + # Special handling for testing command + if ($1 == "testing") { + # Collect the description string + if ($2 ~ /^".*"$/) { # Check if the second token is a quoted string + test_desc = substr($2, 2, length($2) - 2) # Remove quotes + print "\nTesting: " test_desc + } else { + print "Error: testing requires a description in quotes" + } + return + } + + # Check for word definitions + if ($1 == ":") { + word_name = $2 + definition = "" + in_definition = 1 + # Remove : and word name from the input + for (i = 3; i <= NF; i++) { + definition = definition " " $i + } + # If we don't find a semicolon, keep reading lines + while (definition !~ /;$/) { # Changed to match semicolon at end + if ((getline) <= 0) break + gsub(/\(.*\)/, "") # Remove comments + gsub(/^[[:space:]]+/, "") + gsub(/[[:space:]]+$/, "") + definition = definition " " $0 + } + # Remove the semicolon + sub(/[[:space:]]*;[[:space:]]*$/, "", definition) + + if (definition != "") { + define_word(word_name, definition) + print "Defined new word: " word_name + } + in_definition = 0 + return + } + + # Process remaining input + for (i = 1; i <= NF; i++) { + if ($i ~ /^-?[0-9]+$/) { + push($i) + } else if ($i in variables) { # Check if it's a variable + push(variables[$i]) # Push the value of the variable onto the stack + } else if ($i in words) { # Check if it's a defined word + if (!in_definition && compile_only[$i]) { + print "Error: '" $i "' is compile-only" + return + } + if (!execute_word($i)) { + print "Error: Unknown command '" $i "'" + } + } else if ($i != "") { # Ignore empty tokens + print "Error: Unknown command '" $i "'" + } + } + } +} + +# Main loop to read commands (for interactive mode) +{ + process_line() +} + +# Add function to handle dot-quote +function print_string(str) { + printf "%s", str +} diff --git a/awk/forth/test.forth b/awk/forth/test.forth new file mode 100644 index 0000000..d746e66 --- /dev/null +++ b/awk/forth/test.forth @@ -0,0 +1,157 @@ +( Basic Forth test suite ) + +testing "Basic stack operations" +5 dup . . test 5 test 5 + +testing "Addition" +3 4 + test 7 + +testing "Subtraction" +10 3 - test 7 + +testing "Multiplication" +6 7 * test 42 + +testing "Division" +20 4 / test 5 + +testing "Stack manipulation - rot" +1 2 3 rot test 1 + +testing "Stack manipulation - drop" +1 2 3 drop test 2 + +testing "Stack manipulation - nip" +1 2 3 nip test 3 + +testing "Stack manipulation - tuck" +1 2 tuck test 2 + +testing "Stack manipulation - over" +1 2 over test 1 + +testing "Variables" +variable x +5 x ! +x @ test 5 +10 x ! +x ? ( should print 10 ) + +testing "Negate" +5 negate test -5 + +testing "Absolute value" +-7 abs test 7 + +testing "Maximum" +3 8 max test 8 + +testing "Minimum" +3 8 min test 3 + +testing "Modulo" +17 5 mod test 2 + +testing "Equality" +5 5 = test 1 +5 6 = test 0 + +testing "Word definition" +: square dup * ; +5 square test 25 + +testing "Complex word definition" +variable counter +: increment-counter counter @ 1 + counter ! ; +5 counter ! +increment-counter +counter @ test 6 + +testing "Basic conditional - if/then" +: test-if-1 ( n -- n ) dup 5 > if ." Greater than 5" then ; +6 test-if-1 test 6 ( should print "Greater than 5" and leave 6 ) +4 test-if-1 test 4 ( should print nothing and leave 4 ) + +testing "Basic conditional - if/else/then" +: test-if-2 ( n -- n ) dup 5 > if ." Greater" else ." Less=" then ; +6 test-if-2 test 6 ( should print "Greater" and leave 6 ) +4 test-if-2 test 4 ( should print "Less=" and leave 4 ) +5 test-if-2 test 5 ( should print "Less=" and leave 5 ) + +testing "Nested conditionals" +: test-if-3 ( n -- n ) + dup 10 > if + dup 20 > if + ." >20 " + then + ." >10 " + then ; +25 test-if-3 test 25 ( should print ">20 >10 " and leave 25 ) +15 test-if-3 test 15 ( should print ">10 " and leave 15 ) +5 test-if-3 test 5 ( should print nothing and leave 5 ) + +testing "Conditional with stack operations" +: test-if-4 ( n -- n n ) + dup 5 > if + dup + then ; +6 test-if-4 swap test 6 test 6 ( should leave 6 6 ) +4 test-if-4 test 4 ( should leave just 4 ) + +testing "Complex nested conditionals" +: test-if-5 ( n -- n ) + dup 0 < if + ." negative " + else + dup 100 > if + ." big " + else + dup 50 > if + ." medium " + else + ." small " + then + then + then ; +-5 test-if-5 test -5 ( should print "negative " ) +150 test-if-5 test 150 ( should print "big " ) +75 test-if-5 test 75 ( should print "medium " ) +25 test-if-5 test 25 ( should print "small " ) + +testing "Conditionals in word definitions" +: abs-test ( n -- |n| ) dup 0 < if negate then ; +-5 abs-test test 5 +5 abs-test test 5 + +testing "Complex conditional word" +: max-test ( n1 n2 -- max ) + 2dup > if + drop + else + nip + then ; +5 3 max-test test 5 +3 5 max-test test 5 + +( Try to use if outside of a definition - should error ) +testing "Compile-only words" +5 4 > if 42 then ( should print error about compile-only word ) + +testing "Comparison operators" +5 3 > test 1 ( 5 > 3 is true ) +3 5 > test 0 ( 3 > 5 is false ) +3 5 < test 1 ( 3 < 5 is true ) +5 3 < test 0 ( 5 < 3 is false ) +5 5 < test 0 ( 5 < 5 is false ) +5 5 > test 0 ( 5 > 5 is false ) + +testing "Comparison in conditionals" +: test-compare ( n -- ) +dup 5 > if ." Greater than 5" else +dup 5 < if ." Less than 5" else +." Equal to 5" then then ; +6 test-compare ( should print "Greater than 5" ) +4 test-compare ( should print "Less than 5" ) +5 test-compare ( should print "Equal to 5" ) + +bye \ No newline at end of file diff --git a/html/voxels/index.html b/html/isometric-bounce/index.html index 4f60ff2..570f247 100644 --- a/html/voxels/index.html +++ b/html/isometric-bounce/index.html @@ -1,7 +1,7 @@ <!DOCTYPE html> -<html> +<html lang="en"> <head> - <title>Isometric Game</title> + <title>Bouncy Isometric Guy</title> <style> body { margin: 0; @@ -11,7 +11,7 @@ display: block; width: 100vw; height: 100vh; - background: #f0f0f0; + background: #bce8ff; } </style> </head> diff --git a/html/isometric-bounce/js/game.js b/html/isometric-bounce/js/game.js new file mode 100644 index 0000000..a1849c8 --- /dev/null +++ b/html/isometric-bounce/js/game.js @@ -0,0 +1,335 @@ +function createGame() { + const state = { + canvas: document.getElementById('gameCanvas'), + ctx: null, + gridSize: 10, + tileWidth: 50, + tileHeight: 25, + offsetX: 0, + offsetY: 0, + particles: [], + lastFrameTime: 0, + player: { + x: 0, + y: 0, + targetX: 0, + targetY: 0, + size: 20, + path: [], + currentWaypoint: null, + jumpHeight: 0, + jumpProgress: 0, + isJumping: false, + startX: 0, + startY: 0 + }, + isHopping: false, + hopProgress: 0 + }; + + state.ctx = state.canvas.getContext('2d'); + + function toIsometric(x, y) { + return { + x: (x - y) * state.tileWidth / 2, + y: (x + y) * state.tileHeight / 2 + }; + } + + function fromIsometric(screenX, screenY) { + const adjustedX = screenX - state.offsetX; + const adjustedY = screenY - state.offsetY; + + const x = (adjustedX / state.tileWidth + adjustedY / state.tileHeight) / 1; + const y = (adjustedY / state.tileHeight - adjustedX / state.tileWidth) / 1; + + return { x: Math.round(x), y: Math.round(y) }; + } + + function resizeCanvas() { + state.canvas.width = window.innerWidth; + state.canvas.height = window.innerHeight; + + state.offsetX = state.canvas.width / 2; + state.offsetY = state.canvas.height / 3; + + const minDimension = Math.min(state.canvas.width, state.canvas.height); + const scaleFactor = minDimension / 800; + state.tileWidth = 50 * scaleFactor; + state.tileHeight = 25 * scaleFactor; + state.player.size = 20 * scaleFactor; + } + + function dustyParticles(x, y) { + const particleCount = 12; + for (let i = 0; i < particleCount; i++) { + const baseAngle = (Math.PI * 2 * i) / particleCount; + const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5; + + const speed = 0.3 + Math.random() * 0.4; + const initialSize = (state.player.size * 0.15) + (Math.random() * state.player.size * 0.15); + const greyValue = 220 + Math.floor(Math.random() * 35); + + state.particles.push({ + x, y, + dx: Math.cos(randomAngle) * speed, + dy: Math.sin(randomAngle) * speed, + life: 0.8 + Math.random() * 0.4, + size: initialSize, + color: `rgb(${greyValue}, ${greyValue}, ${greyValue})`, + initialSize, + rotationSpeed: (Math.random() - 0.5) * 0.2, + rotation: Math.random() * Math.PI * 2 + }); + } + } + + function updateParticles() { + for (let i = state.particles.length - 1; i >= 0; i--) { + const particle = state.particles[i]; + + particle.x += particle.dx; + particle.y += particle.dy; + particle.dy += 0.01; + particle.rotation += particle.rotationSpeed; + particle.life -= 0.03; + particle.size = particle.initialSize * (particle.life * 1.5); + + if (particle.life <= 0) { + state.particles.splice(i, 1); + } + } + } + + function findPath(startX, startY, endX, endY) { + const path = []; + + if (startX !== endX) { + const stepX = startX < endX ? 1 : -1; + for (let x = startX + stepX; stepX > 0 ? x <= endX : x >= endX; x += stepX) { + path.push({ x, y: startY }); + } + } + + if (startY !== endY) { + const stepY = startY < endY ? 1 : -1; + for (let y = startY + stepY; stepY > 0 ? y <= endY : y >= endY; y += stepY) { + path.push({ x: endX, y }); + } + } + + return path; + } + + function updatePlayer() { + const jumpDuration = 0.1; + const maxJumpHeight = state.tileHeight; + + if (state.isHopping) { + state.hopProgress += jumpDuration; + state.hopProgress = Math.min(state.hopProgress, 1); + + state.player.jumpHeight = Math.sin(state.hopProgress * Math.PI) * maxJumpHeight; + + if (state.hopProgress >= 1) { + state.isHopping = false; + state.player.jumpHeight = 0; + } + } else { + if (!state.player.currentWaypoint && state.player.path.length > 0) { + state.player.currentWaypoint = state.player.path.shift(); + state.player.isJumping = true; + state.player.jumpProgress = 0; + state.player.startX = state.player.x; + state.player.startY = state.player.y; + } + + if (state.player.currentWaypoint && state.player.isJumping) { + state.player.jumpProgress += jumpDuration; + state.player.jumpProgress = Math.min(state.player.jumpProgress, 1); + + state.player.jumpHeight = Math.sin(state.player.jumpProgress * Math.PI) * maxJumpHeight; + + state.player.x = state.player.startX + (state.player.currentWaypoint.x - state.player.startX) * state.player.jumpProgress; + state.player.y = state.player.startY + (state.player.currentWaypoint.y - state.player.startY) * state.player.jumpProgress; + + if (state.player.jumpProgress >= 1) { + state.player.isJumping = false; + state.player.jumpHeight = 0; + state.player.x = state.player.currentWaypoint.x; + state.player.y = state.player.currentWaypoint.y; + dustyParticles(state.player.x, state.player.y); + state.player.currentWaypoint = null; + } + } + } + } + + function drawGrid() { + for (let x = 0; x < state.gridSize; x++) { + for (let y = 0; y < state.gridSize; y++) { + const iso = toIsometric(x, y); + + // Diamonds! + state.ctx.beginPath(); // Start a new path + state.ctx.moveTo(iso.x + state.offsetX, iso.y + state.offsetY - state.tileHeight/2); // Move to the top point of the diamond + state.ctx.lineTo(iso.x + state.offsetX + state.tileWidth/2, iso.y + state.offsetY); // Draw line to the right point of the diamond + state.ctx.lineTo(iso.x + state.offsetX, iso.y + state.offsetY + state.tileHeight/2); // Draw line to the bottom point of the diamond + state.ctx.lineTo(iso.x + state.offsetX - state.tileWidth/2, iso.y + state.offsetY); // Draw line to the left point of the diamond + state.ctx.closePath(); // Close the path to complete the diamond + + state.ctx.strokeStyle = '#666'; + state.ctx.stroke(); + state.ctx.fillStyle = '#fff'; + state.ctx.fill(); + } + } + } + + function drawParticles() { + state.particles.forEach(particle => { + const iso = toIsometric(particle.x, particle.y); + + state.ctx.save(); + state.ctx.translate(iso.x + state.offsetX, iso.y + state.offsetY); + state.ctx.rotate(particle.rotation); + + state.ctx.beginPath(); + const points = 5; + for (let i = 0; i < points * 2; i++) { + const angle = (i * Math.PI) / points; + const radius = particle.size * (i % 2 ? 0.7 : 1); + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + i === 0 ? state.ctx.moveTo(x, y) : state.ctx.lineTo(x, y); + } + state.ctx.closePath(); + + state.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; + state.ctx.fill(); + + state.ctx.restore(); + }); + } + + function drawPlayer() { + const iso = toIsometric(state.player.x, state.player.y); + const jumpOffset = state.player.jumpHeight || state.player.jumpHeight; + + let squashStretch = 1; + if (state.player.isJumping) { + const jumpPhase = Math.sin(state.player.jumpProgress * Math.PI); + if (state.player.jumpProgress < 0.2) { + squashStretch = 1 + (0.3 * (1 - state.player.jumpProgress / 0.2)); + } else if (state.player.jumpProgress > 0.8) { + squashStretch = 1 - (0.3 * ((state.player.jumpProgress - 0.8) / 0.2)); + } else { + squashStretch = 1 + (0.1 * jumpPhase); + } + } + + const shadowScale = Math.max(0.2, 1 - (jumpOffset / state.tileHeight)); + state.ctx.beginPath(); + state.ctx.ellipse( + iso.x + state.offsetX, + iso.y + state.offsetY + 2, + state.player.size * 0.8 * shadowScale, + state.player.size * 0.3 * shadowScale, + 0, 0, Math.PI * 2 + ); + state.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`; + state.ctx.fill(); + + const bodyHeight = state.player.size * 2 * squashStretch; + const bodyWidth = state.player.size * 0.8 * (1 / squashStretch); + + state.ctx.save(); + state.ctx.translate(iso.x + state.offsetX, iso.y + state.offsetY - jumpOffset); + state.ctx.scale(1, 0.5); + state.ctx.fillStyle = '#ff4444'; + state.ctx.strokeStyle = '#aa0000'; + state.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); + state.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); + state.ctx.restore(); + + state.ctx.beginPath(); + state.ctx.ellipse( + iso.x + state.offsetX, + iso.y + state.offsetY - state.player.size * squashStretch - jumpOffset, + state.player.size * (1 / squashStretch), + state.player.size * 0.5 * squashStretch, + 0, 0, Math.PI * 2 + ); + state.ctx.fillStyle = '#ff4444'; + state.ctx.fill(); + state.ctx.strokeStyle = '#aa0000'; + state.ctx.stroke(); + } + + function gameLoop(timestamp) { + + const frameInterval = 1000 / 60; + + if (!state.lastFrameTime || timestamp - state.lastFrameTime >= frameInterval) { + state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height); + + drawGrid(); + updateParticles(); + drawParticles(); + updatePlayer(); + drawPlayer(); + + state.lastFrameTime = timestamp; + } + + requestAnimationFrame(gameLoop); + } + + function handleClick(e) { + const rect = state.canvas.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + const gridPos = fromIsometric(clickX, clickY); + + const iso = toIsometric(state.player.x, state.player.y); + const playerRadius = state.player.size; + const distanceToPlayer = Math.sqrt( + Math.pow(clickX - (iso.x + state.offsetX), 2) + + Math.pow(clickY - (iso.y + state.offsetY), 2) + ); + + if (distanceToPlayer < playerRadius) { + state.isHopping = true; + state.hopProgress = 0; + } else if (gridPos.x >= 0 && gridPos.x < state.gridSize && + gridPos.y >= 0 && gridPos.y < state.gridSize) { + state.player.targetX = Math.round(gridPos.x); + state.player.targetY = Math.round(gridPos.y); + + state.player.path = findPath( + Math.round(state.player.x), + Math.round(state.player.y), + state.player.targetX, + state.player.targetY + ); + + state.player.currentWaypoint = null; + } + } + + function init() { + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + state.canvas.addEventListener('click', handleClick); + state.lastFrameTime = 0; + gameLoop(); + } + + return { init }; +} + +window.onload = () => { + const game = createGame(); + game.init(); +}; \ No newline at end of file diff --git a/html/voxels/js/game.js b/html/voxels/js/game.js deleted file mode 100644 index 4547634..0000000 --- a/html/voxels/js/game.js +++ /dev/null @@ -1,345 +0,0 @@ -class IsometricGame { - constructor() { - this.canvas = document.getElementById('gameCanvas'); - this.ctx = this.canvas.getContext('2d'); - - // Grid properties - this.gridSize = 10; - this.tileWidth = 50; - this.tileHeight = 25; - - // Player properties - this.player = { - x: 0, - y: 0, - targetX: 0, - targetY: 0, - size: 20, - path: [], // Array to store waypoints - currentWaypoint: null, - jumpHeight: 0, - jumpProgress: 0, - isJumping: false, - startX: 0, - startY: 0 - }; - - // Add particle system - this.particles = []; - - // Handle window resize - this.resizeCanvas(); - window.addEventListener('resize', () => this.resizeCanvas()); - - this.setupEventListeners(); - this.gameLoop(); - } - - resizeCanvas() { - this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight; - - // Recalculate grid offset to center it - this.offsetX = this.canvas.width / 2; - this.offsetY = this.canvas.height / 3; - - // Scale tile size based on screen size - const minDimension = Math.min(this.canvas.width, this.canvas.height); - const scaleFactor = minDimension / 800; // 800 is our reference size - this.tileWidth = 50 * scaleFactor; - this.tileHeight = 25 * scaleFactor; - this.player.size = 20 * scaleFactor; - } - - toIsometric(x, y) { - return { - x: (x - y) * this.tileWidth / 2, - y: (x + y) * this.tileHeight / 2 - }; - } - - fromIsometric(screenX, screenY) { - // Convert screen coordinates back to grid coordinates - screenX -= this.offsetX; - screenY -= this.offsetY; - - const x = (screenX / this.tileWidth + screenY / this.tileHeight) / 1; - const y = (screenY / this.tileHeight - screenX / this.tileWidth) / 1; - - return { x: Math.round(x), y: Math.round(y) }; - } - - drawGrid() { - for (let x = 0; x < this.gridSize; x++) { - for (let y = 0; y < this.gridSize; y++) { - const iso = this.toIsometric(x, y); - - // Draw tile - this.ctx.beginPath(); - this.ctx.moveTo(iso.x + this.offsetX, iso.y + this.offsetY - this.tileHeight/2); - this.ctx.lineTo(iso.x + this.offsetX + this.tileWidth/2, iso.y + this.offsetY); - this.ctx.lineTo(iso.x + this.offsetX, iso.y + this.offsetY + this.tileHeight/2); - this.ctx.lineTo(iso.x + this.offsetX - this.tileWidth/2, iso.y + this.offsetY); - this.ctx.closePath(); - - this.ctx.strokeStyle = '#666'; - this.ctx.stroke(); - this.ctx.fillStyle = '#fff'; - this.ctx.fill(); - } - } - } - - drawPlayer() { - // Convert player grid position to isometric coordinates - const iso = this.toIsometric(this.player.x, this.player.y); - - // Apply jump height offset - const jumpOffset = this.player.jumpHeight || 0; - - // Draw player shadow (gets smaller when jumping) - const shadowScale = Math.max(0.2, 1 - (jumpOffset / this.tileHeight)); - this.ctx.beginPath(); - this.ctx.ellipse( - iso.x + this.offsetX, - iso.y + this.offsetY + 2, - this.player.size * 0.8 * shadowScale, - this.player.size * 0.3 * shadowScale, - 0, - 0, - Math.PI * 2 - ); - this.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`; - this.ctx.fill(); - - // Draw player body with jump offset - this.ctx.beginPath(); - this.ctx.fillStyle = '#ff4444'; - this.ctx.strokeStyle = '#aa0000'; - - const bodyHeight = this.player.size * 2; - const bodyWidth = this.player.size * 0.8; - - this.ctx.save(); - this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY - jumpOffset); - this.ctx.scale(1, 0.5); - this.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); - this.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); - this.ctx.restore(); - - // Draw player head with jump offset - this.ctx.beginPath(); - this.ctx.ellipse( - iso.x + this.offsetX, - iso.y + this.offsetY - this.player.size - jumpOffset, - this.player.size, - this.player.size * 0.5, - 0, - 0, - Math.PI * 2 - ); - this.ctx.fillStyle = '#ff4444'; - this.ctx.fill(); - this.ctx.strokeStyle = '#aa0000'; - this.ctx.stroke(); - } - - findPath(startX, startY, endX, endY) { - // Simple pathfinding that follows grid edges - const path = []; - - // First move along X axis - if (startX !== endX) { - const stepX = startX < endX ? 1 : -1; - for (let x = startX + stepX; stepX > 0 ? x <= endX : x >= endX; x += stepX) { - path.push({ x: x, y: startY }); - } - } - - // Then move along Y axis - if (startY !== endY) { - const stepY = startY < endY ? 1 : -1; - for (let y = startY + stepY; stepY > 0 ? y <= endY : y >= endY; y += stepY) { - path.push({ x: endX, y: y }); - } - } - - return path; - } - - updatePlayer() { - const jumpDuration = 0.1; // Faster jump for snappier movement - const maxJumpHeight = this.tileHeight; - - // If we don't have a current waypoint but have a path, get next waypoint - if (!this.player.currentWaypoint && this.player.path.length > 0) { - this.player.currentWaypoint = this.player.path.shift(); - this.player.isJumping = true; - this.player.jumpProgress = 0; - - // Store starting position for interpolation - this.player.startX = this.player.x; - this.player.startY = this.player.y; - } - - // Move towards current waypoint - if (this.player.currentWaypoint) { - // Update jump animation - if (this.player.isJumping) { - this.player.jumpProgress += jumpDuration; - - // Clamp progress to 1 - if (this.player.jumpProgress > 1) this.player.jumpProgress = 1; - - // Parabolic jump arc - this.player.jumpHeight = Math.sin(this.player.jumpProgress * Math.PI) * maxJumpHeight; - - // Precise interpolation between points - this.player.x = this.player.startX + (this.player.currentWaypoint.x - this.player.startX) * this.player.jumpProgress; - this.player.y = this.player.startY + (this.player.currentWaypoint.y - this.player.startY) * this.player.jumpProgress; - - // Landing - if (this.player.jumpProgress >= 1) { - this.player.isJumping = false; - this.player.jumpHeight = 0; - this.player.x = this.player.currentWaypoint.x; - this.player.y = this.player.currentWaypoint.y; - this.createDustParticles(this.player.x, this.player.y); - this.player.currentWaypoint = null; - } - } - } - } - - setupEventListeners() { - this.canvas.addEventListener('click', (e) => { - const rect = this.canvas.getBoundingClientRect(); - const clickX = e.clientX - rect.left; - const clickY = e.clientY - rect.top; - - const gridPos = this.fromIsometric(clickX, clickY); - - // Only move if within grid bounds - if (gridPos.x >= 0 && gridPos.x < this.gridSize && - gridPos.y >= 0 && gridPos.y < this.gridSize) { - - // Set target and calculate path - this.player.targetX = Math.round(gridPos.x); - this.player.targetY = Math.round(gridPos.y); - - // Calculate new path - this.player.path = this.findPath( - Math.round(this.player.x), - Math.round(this.player.y), - this.player.targetX, - this.player.targetY - ); - - // Clear current waypoint to start new path - this.player.currentWaypoint = null; - } - }); - } - - gameLoop() { - // Clear canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // Draw game elements - this.drawGrid(); - this.updateParticles(); - this.drawParticles(); - this.updatePlayer(); - this.drawPlayer(); - - // Continue game loop - requestAnimationFrame(() => this.gameLoop()); - } - - // Add new particle system methods - createDustParticles(x, y) { - const particleCount = 12; // Increased for more particles - for (let i = 0; i < particleCount; i++) { - // Randomize the angle slightly - const baseAngle = (Math.PI * 2 * i) / particleCount; - const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5; - - // Random speed and size variations - const speed = 0.3 + Math.random() * 0.4; - const initialSize = (this.player.size * 0.15) + (Math.random() * this.player.size * 0.15); - - // Random grey color - const greyValue = 220 + Math.floor(Math.random() * 35); - const color = `rgb(${greyValue}, ${greyValue}, ${greyValue})`; - - this.particles.push({ - x: x, - y: y, - dx: Math.cos(randomAngle) * speed, - dy: Math.sin(randomAngle) * speed, - life: 0.8 + Math.random() * 0.4, // Random initial life - size: initialSize, - color: color, - initialSize: initialSize, - rotationSpeed: (Math.random() - 0.5) * 0.2, - rotation: Math.random() * Math.PI * 2 - }); - } - } - - updateParticles() { - for (let i = this.particles.length - 1; i >= 0; i--) { - const particle = this.particles[i]; - - // Update position with slight gravity effect - particle.x += particle.dx; - particle.y += particle.dy; - particle.dy += 0.01; // Slight upward drift - - // Update rotation - particle.rotation += particle.rotationSpeed; - - // Non-linear fade out - particle.life -= 0.03; - particle.size = particle.initialSize * (particle.life * 1.5); // Grow slightly as they fade - - // Remove dead particles - if (particle.life <= 0) { - this.particles.splice(i, 1); - } - } - } - - drawParticles() { - for (const particle of this.particles) { - const iso = this.toIsometric(particle.x, particle.y); - - this.ctx.save(); - this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY); - this.ctx.rotate(particle.rotation); - - // Draw a slightly irregular dust puff - this.ctx.beginPath(); - const points = 5; - for (let i = 0; i < points * 2; i++) { - const angle = (i * Math.PI) / points; - const radius = particle.size * (i % 2 ? 0.7 : 1); - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius; - if (i === 0) this.ctx.moveTo(x, y); - else this.ctx.lineTo(x, y); - } - this.ctx.closePath(); - - this.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; - this.ctx.fill(); - - this.ctx.restore(); - } - } -} - -// Start the game when the page loads -window.onload = () => { - new IsometricGame(); -}; \ No newline at end of file |