Implement Nova Farma S1 Max: Layered terrain, water mechanics, Y-sorting, and asset cleanup
BIN
assets/.DS_Store
vendored
BIN
assets/DEMO_FAZA1/.DS_Store
vendored
BIN
assets/DEMO_FAZA1/Characters/Kai_Dreads.png
Normal file
|
After Width: | Height: | Size: 776 KiB |
BIN
assets/DEMO_FAZA1/Environment/.DS_Store
vendored
Normal file
BIN
assets/DEMO_FAZA1/Environment/mud_puddle.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
assets/DEMO_FAZA1/Environment/obstacle_thorns.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
assets/DEMO_FAZA1/Environment/sign_danger.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
assets/DEMO_FAZA1/Environment/stream_water.png
Normal file
|
After Width: | Height: | Size: 387 KiB |
BIN
assets/DEMO_FAZA1/Environment/water_clean_patch.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
assets/DEMO_FAZA1/Ground/ground_dirt_patch.png
Normal file
|
After Width: | Height: | Size: 576 KiB |
BIN
assets/DEMO_FAZA1/Ground/ground_pebbles.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
BIN
assets/DEMO_FAZA1/Items/.DS_Store
vendored
Normal file
BIN
assets/DEMO_FAZA1/Items/hay_drop_0.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/DEMO_FAZA1/Items/item_longboard_wheel.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/DEMO_FAZA1/Structures/foundation_concrete.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/DEMO_FAZA1/UI/badge_purchase.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
assets/DEMO_FAZA1/UI/badge_trial.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
assets/DEMO_FAZA1/UI/health_critical.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
BIN
assets/DEMO_FAZA1/VFX/overlay_amnesia_blood.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/DEMO_FAZA1/VFX/toxic_fog.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
assets/DEMO_FAZA1/Vegetation/bush_hiding_spot.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 235 KiB |
BIN
assets/DEMO_FAZA1/Vegetation/grass_cluster_dense.png
Normal file
|
After Width: | Height: | Size: 983 KiB |
BIN
assets/DEMO_FAZA1/Vegetation/grass_cluster_flowery.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -63,3 +63,23 @@
|
|||||||
* **Construction:** Easy to build – just dig a hole and place a marker.
|
* **Construction:** Easy to build – just dig a hole and place a marker.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 8. Planned Asset Updates (To-Do)
|
||||||
|
These assets are priorities for future updates to match the "Dark Chibi Noir" style:
|
||||||
|
|
||||||
|
### Survival & Base
|
||||||
|
* **Power Generator (Agregat):** Modern, rusty industrial look. Needs a broken state (repair quest) and working state.
|
||||||
|
* **Workbench (Delovna miza):** Cluttered with tools, blueprints, and scrap. Visual progression indicator.
|
||||||
|
* **Campfire (Ogenj):** Central resting point. Needs animated flames.
|
||||||
|
* **Sleeping Bag/Tent:** Essential for saving/sleeping.
|
||||||
|
|
||||||
|
### Farming & Water
|
||||||
|
* **Tilled Soil (Gredica):** Basic dark earth tile for planting.
|
||||||
|
* **Well (Vodnjak):** Source of clean water (distinct from the dirty stream).
|
||||||
|
* **Water Barrel (Sod):** For collecting rainwater.
|
||||||
|
|
||||||
|
### Defense & Structures
|
||||||
|
* **Modular Fencing:** Corner, straight, and broken pieces for custom enclosures.
|
||||||
|
* **Traps:** Bear traps or spike pits for defense.
|
||||||
|
* **Dumpster:** Rusty container for looting.
|
||||||
|
* **Compost Pile:** Organic waste management.
|
||||||
|
|||||||
59
scripts/clean_and_resize_batch_2.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def remove_white_bg_and_resize(path, target_width=None):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"File not found: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.imread(path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Error loading {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||||
|
|
||||||
|
# Threshold for white
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
|
||||||
|
_, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
|
||||||
|
mask_inv = cv2.bitwise_not(mask)
|
||||||
|
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
final_img = cv2.merge((b, g, r, mask_inv))
|
||||||
|
|
||||||
|
# Crop
|
||||||
|
coords = cv2.findNonZero(mask_inv)
|
||||||
|
if coords is not None:
|
||||||
|
x, y, w, h = cv2.boundingRect(coords)
|
||||||
|
pad = 2
|
||||||
|
x = max(0, x - pad); y = max(0, y - pad)
|
||||||
|
w = min(final_img.shape[1] - x, w + 2*pad)
|
||||||
|
h = min(final_img.shape[0] - y, h + 2*pad)
|
||||||
|
final_img = final_img[y:y+h, x:x+w]
|
||||||
|
|
||||||
|
# Resize
|
||||||
|
if target_width:
|
||||||
|
h_curr, w_curr = final_img.shape[:2]
|
||||||
|
if w_curr > target_width:
|
||||||
|
ratio = target_width / w_curr
|
||||||
|
new_h = int(h_curr * ratio)
|
||||||
|
final_img = cv2.resize(final_img, (target_width, new_h), interpolation=cv2.INTER_AREA)
|
||||||
|
print(f"Resized {path} to {target_width}x{new_h}")
|
||||||
|
|
||||||
|
cv2.imwrite(path, final_img)
|
||||||
|
print(f"Processed: {path}")
|
||||||
|
|
||||||
|
# DEFINIRAJ NOVE CILJE
|
||||||
|
TARGETS = {
|
||||||
|
'assets/DEMO_FAZA1/Environment/sign_danger.png': 256,
|
||||||
|
'assets/DEMO_FAZA1/UI/badge_trial.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Items/item_longboard_wheel.png': 128,
|
||||||
|
'assets/DEMO_FAZA1/VFX/overlay_amnesia_blood.png': 1024 # Overlay stays big
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = os.getcwd()
|
||||||
|
for rel, w in TARGETS.items():
|
||||||
|
remove_white_bg_and_resize(os.path.join(base, rel), w)
|
||||||
83
scripts/clean_and_resize_white.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def remove_white_bg_and_resize(path, target_width=None):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"File not found: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.imread(path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Error loading {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert to RGBA
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||||
|
|
||||||
|
# Threshold for white (allowing slight off-white/compression artifacts)
|
||||||
|
# White is (255,255,255). Let's say anything > 240 in all channels.
|
||||||
|
lower_white = np.array([240, 240, 240, 255])
|
||||||
|
upper_white = np.array([255, 255, 255, 255])
|
||||||
|
|
||||||
|
# Create mask: looking for white pixels
|
||||||
|
# Note: cv2.inRange checks ranges. For BGRA, A is 255.
|
||||||
|
# However, input usually doesn't have A yet if loaded as BGR.
|
||||||
|
# But we converted.
|
||||||
|
|
||||||
|
# Alternative: Grayscale thresholding is often safer for "almost white" backgrounds in illustrations
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
|
||||||
|
# Create binary mask where white is 255, else 0
|
||||||
|
_, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
|
# Invert mask: We want to KEEP non-white.
|
||||||
|
# Non-white = 0 in mask? No, thresholded white became 255.
|
||||||
|
# So we invert.
|
||||||
|
mask_inv = cv2.bitwise_not(mask)
|
||||||
|
|
||||||
|
# Morphological cleanup (remove small white specks inside object if necessary, or smooth edges)
|
||||||
|
# For vector art, simple threshold usually works well.
|
||||||
|
|
||||||
|
# Assign new alpha channel
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
# Use mask_inv as alpha
|
||||||
|
final_img = cv2.merge((b, g, r, mask_inv))
|
||||||
|
|
||||||
|
# Crop to content (Trim transparent borders)
|
||||||
|
coords = cv2.findNonZero(mask_inv)
|
||||||
|
if coords is not None:
|
||||||
|
x, y, w, h = cv2.boundingRect(coords)
|
||||||
|
# Padding
|
||||||
|
pad = 2
|
||||||
|
x = max(0, x - pad)
|
||||||
|
y = max(0, y - pad)
|
||||||
|
w = min(final_img.shape[1] - x, w + 2*pad)
|
||||||
|
h = min(final_img.shape[0] - y, h + 2*pad)
|
||||||
|
final_img = final_img[y:y+h, x:x+w]
|
||||||
|
|
||||||
|
# Resize if target_width provided
|
||||||
|
if target_width:
|
||||||
|
h_curr, w_curr = final_img.shape[:2]
|
||||||
|
if w_curr > target_width: # Only scale down
|
||||||
|
ratio = target_width / w_curr
|
||||||
|
new_h = int(h_curr * ratio)
|
||||||
|
final_img = cv2.resize(final_img, (target_width, new_h), interpolation=cv2.INTER_AREA)
|
||||||
|
print(f"Resized to {target_width}x{new_h}")
|
||||||
|
|
||||||
|
cv2.imwrite(path, final_img)
|
||||||
|
print(f"Processed (Cleaned White BG + Trim): {path}")
|
||||||
|
|
||||||
|
TARGETS = {
|
||||||
|
'assets/DEMO_FAZA1/UI/badge_purchase.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/UI/health_critical.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/VFX/toxic_fog.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Environment/obstacle_thorns.png': 256,
|
||||||
|
'assets/DEMO_FAZA1/Structures/foundation_concrete.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Items/hay_drop_0.png': 128
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = os.getcwd()
|
||||||
|
for rel, w in TARGETS.items():
|
||||||
|
remove_white_bg_and_resize(os.path.join(base, rel), w)
|
||||||
@@ -1,59 +1,59 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def remove_pink_bg():
|
def clean_pink_and_soften(input_path, output_path):
|
||||||
input_path = '/Users/davidkotnik/.gemini/antigravity/brain/63340bd3-91a9-439d-b1d9-5692ce5adaea/visoka_trava_v2_pink_bg_1769436757738.png'
|
print(f"Processing {input_path}...")
|
||||||
output_path = 'assets/DEMO_FAZA1/Vegetation/visoka_trava_v2.png'
|
|
||||||
|
|
||||||
# Ensure directory exists
|
img = cv2.imread(input_path, cv2.IMREAD_UNCHANGED)
|
||||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
||||||
|
|
||||||
# Read image
|
|
||||||
img = cv2.imread(input_path)
|
|
||||||
if img is None:
|
if img is None:
|
||||||
print(f"Error: Could not read {input_path}")
|
print("Failed to load image.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Convert to RGBA
|
# Check channels
|
||||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
if img.shape[2] == 3:
|
||||||
|
b, g, r = cv2.split(img)
|
||||||
|
# Create alpha (255)
|
||||||
|
a = np.ones_like(b) * 255
|
||||||
|
img = cv2.merge((b, g, r, a))
|
||||||
|
|
||||||
# Define Magenta range (B, G, R) - standard magenta is (255, 0, 255) in BGR
|
# Targeted Pink Range in HSV
|
||||||
# We'll allow a small tolerance to catch anti-aliasing edges
|
hsv = cv2.cvtColor(img[:,:,0:3], cv2.COLOR_BGR2HSV)
|
||||||
sensitivity = 20
|
lower_pink = np.array([145, 50, 50])
|
||||||
lower_magenta = np.array([255 - sensitivity, 0, 255 - sensitivity])
|
upper_pink = np.array([170, 255, 255])
|
||||||
upper_magenta = np.array([255, sensitivity, 255])
|
|
||||||
|
|
||||||
# Create mask
|
pink_mask = cv2.inRange(hsv, lower_pink, upper_pink)
|
||||||
# Note: OpenCV reads as BGR, so Pink #FF00FF is (255, 0, 255)
|
|
||||||
# Using simple color thresholding
|
|
||||||
mask = cv2.inRange(img[:,:,:3], lower_magenta, upper_magenta)
|
|
||||||
|
|
||||||
# Invert mask (we want to keep non-magenta)
|
# 1. REMOVE PINK (Set alpha to 0)
|
||||||
mask_inv = cv2.bitwise_not(mask)
|
# Get current alpha
|
||||||
|
|
||||||
# Apply mask to alpha channel
|
|
||||||
# Where mask is white (magenta pixels), alpha becomes 0
|
|
||||||
# But mask_inv is white for KEEP pixels.
|
|
||||||
# So we want Alpha to be 0 where mask is 255.
|
|
||||||
|
|
||||||
# Better approach:
|
|
||||||
# 1. Split channels
|
|
||||||
b, g, r, a = cv2.split(img)
|
b, g, r, a = cv2.split(img)
|
||||||
|
|
||||||
# 2. Update alpha channel using the inverted mask
|
# Where pink_mask is 255, set alpha to 0
|
||||||
# If mask pixel is 255 (magenta), mask_inv is 0. We want alpha 0 there.
|
# Also combine with existing alpha (if image was already transparent, keep it)
|
||||||
# If mask pixel is 0 (not magenta), mask_inv is 255. We want alpha 255 there.
|
new_alpha = cv2.bitwise_and(a, cv2.bitwise_not(pink_mask))
|
||||||
# However, original alpha is 255 everywhere inside the image bounds.
|
|
||||||
# So we can just take bitwise_and or just set alpha to mask_inv.
|
|
||||||
|
|
||||||
img[:, :, 3] = mask_inv
|
|
||||||
|
|
||||||
# Optional: Basic edge smoothing/despiking if needed, but for pixel/vector art simple cut is often better.
|
# 2. EDGE SOFTENING (Alpha Blur 1px)
|
||||||
|
# We blur the alpha channel slightly to soften jagged edges
|
||||||
|
soft_alpha = cv2.GaussianBlur(new_alpha, (3, 3), 0)
|
||||||
|
|
||||||
# Save
|
# Thresholding back to keep semi-transparency only on very edge?
|
||||||
cv2.imwrite(output_path, img)
|
# Or just keep the blur. For "Vector Style", usually we want sharp, but
|
||||||
print(f"Successfully saved transparent image to {output_path}")
|
# user asked for "zmehčaj robove (Alpha Blur: 1px)".
|
||||||
|
# Gaussian (3,3) is roughly 1px radius.
|
||||||
|
|
||||||
|
# 3. COLOR BLEED REMOVAL (Halo effect)
|
||||||
|
# Often pink pixels leave a pink fringe.
|
||||||
|
# We can dilate the non-pink color into the transparent area?
|
||||||
|
# Or simpler: desaturate or darken pixels on the edge of transparency.
|
||||||
|
# For now, let's stick to the requested Blur.
|
||||||
|
|
||||||
|
final_img = cv2.merge((b, g, r, soft_alpha))
|
||||||
|
|
||||||
|
cv2.imwrite(output_path, final_img)
|
||||||
|
print(f"Saved cleaned image to {output_path}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
remove_pink_bg()
|
# Hardcoded path as per instructions: Vegetation/visoka_trava_v2.png
|
||||||
|
target = 'assets/DEMO_FAZA1/Vegetation/visoka_trava_v2.png'
|
||||||
|
clean_pink_and_soften(target, target)
|
||||||
|
|||||||
56
scripts/clean_single_water.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def remove_white_bg_and_resize(path, target_width=None):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"File not found: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.imread(path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Error loading {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||||
|
|
||||||
|
# Threshold for white
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
|
||||||
|
_, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
|
||||||
|
mask_inv = cv2.bitwise_not(mask)
|
||||||
|
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
final_img = cv2.merge((b, g, r, mask_inv))
|
||||||
|
|
||||||
|
# Crop
|
||||||
|
coords = cv2.findNonZero(mask_inv)
|
||||||
|
if coords is not None:
|
||||||
|
x, y, w, h = cv2.boundingRect(coords)
|
||||||
|
pad = 2
|
||||||
|
x = max(0, x - pad); y = max(0, y - pad)
|
||||||
|
w = min(final_img.shape[1] - x, w + 2*pad)
|
||||||
|
h = min(final_img.shape[0] - y, h + 2*pad)
|
||||||
|
final_img = final_img[y:y+h, x:x+w]
|
||||||
|
|
||||||
|
# Resize
|
||||||
|
if target_width:
|
||||||
|
h_curr, w_curr = final_img.shape[:2]
|
||||||
|
if w_curr > target_width:
|
||||||
|
ratio = target_width / w_curr
|
||||||
|
new_h = int(h_curr * ratio)
|
||||||
|
final_img = cv2.resize(final_img, (target_width, new_h), interpolation=cv2.INTER_AREA)
|
||||||
|
print(f"Resized {path} to {target_width}x{new_h}")
|
||||||
|
|
||||||
|
cv2.imwrite(path, final_img)
|
||||||
|
print(f"Processed: {path}")
|
||||||
|
|
||||||
|
# SAMO NOVA ZADNJA SLIKA
|
||||||
|
TARGETS = {
|
||||||
|
'assets/DEMO_FAZA1/Environment/water_clean_patch.png': 512
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = os.getcwd()
|
||||||
|
for rel, w in TARGETS.items():
|
||||||
|
remove_white_bg_and_resize(os.path.join(base, rel), w)
|
||||||
57
scripts/clean_water_assets.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def remove_white_bg_and_resize(path, target_width=None):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"File not found: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.imread(path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Error loading {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||||
|
|
||||||
|
# Threshold for white
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
|
||||||
|
_, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
|
||||||
|
mask_inv = cv2.bitwise_not(mask)
|
||||||
|
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
final_img = cv2.merge((b, g, r, mask_inv))
|
||||||
|
|
||||||
|
# Crop
|
||||||
|
coords = cv2.findNonZero(mask_inv)
|
||||||
|
if coords is not None:
|
||||||
|
x, y, w, h = cv2.boundingRect(coords)
|
||||||
|
pad = 2
|
||||||
|
x = max(0, x - pad); y = max(0, y - pad)
|
||||||
|
w = min(final_img.shape[1] - x, w + 2*pad)
|
||||||
|
h = min(final_img.shape[0] - y, h + 2*pad)
|
||||||
|
final_img = final_img[y:y+h, x:x+w]
|
||||||
|
|
||||||
|
# Resize
|
||||||
|
if target_width:
|
||||||
|
h_curr, w_curr = final_img.shape[:2]
|
||||||
|
if w_curr > target_width:
|
||||||
|
ratio = target_width / w_curr
|
||||||
|
new_h = int(h_curr * ratio)
|
||||||
|
final_img = cv2.resize(final_img, (target_width, new_h), interpolation=cv2.INTER_AREA)
|
||||||
|
print(f"Resized {path} to {target_width}x{new_h}")
|
||||||
|
|
||||||
|
cv2.imwrite(path, final_img)
|
||||||
|
print(f"Processed: {path}")
|
||||||
|
|
||||||
|
# NOVI CILJI (Voda)
|
||||||
|
TARGETS = {
|
||||||
|
'assets/DEMO_FAZA1/Environment/mud_puddle.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Environment/stream_water.png': 512
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = os.getcwd()
|
||||||
|
for rel, w in TARGETS.items():
|
||||||
|
remove_white_bg_and_resize(os.path.join(base, rel), w)
|
||||||
43
scripts/fix_pink_edges.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
def fix_pink_edges(directory):
|
||||||
|
print(f"Scanning {directory} for pink edges...")
|
||||||
|
|
||||||
|
# Target specific problematic file if needed, or all
|
||||||
|
files = glob.glob(os.path.join(directory, "*.png"))
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
||||||
|
if img is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if img.shape[2] < 4:
|
||||||
|
continue
|
||||||
|
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
|
||||||
|
# 1. Wider Pink/Magenta Range
|
||||||
|
# Catch anything with high Red/Blue and relatively lower Green
|
||||||
|
pink_mask = (r > 150) & (g < 100) & (b > 150)
|
||||||
|
|
||||||
|
# Turn pink pixels transparent
|
||||||
|
a[pink_mask] = 0
|
||||||
|
|
||||||
|
# 2. Aggressive Alpha Erosion
|
||||||
|
# Increase iterations to eat 2 pixels into the edge
|
||||||
|
# This removes the "halo" left by anti-aliasing against pink
|
||||||
|
kernel = np.ones((3,3), np.uint8)
|
||||||
|
a_eroded = cv2.erode(a, kernel, iterations=1) # 1 iteration with 3x3 kernel = 1-2px border
|
||||||
|
|
||||||
|
# Merge back
|
||||||
|
img_fixed = cv2.merge((b, g, r, a_eroded))
|
||||||
|
|
||||||
|
cv2.imwrite(path, img_fixed)
|
||||||
|
print(f"Aggressively Fixed: {os.path.basename(path)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
target_dir = "assets/DEMO_FAZA1/Vegetation"
|
||||||
|
fix_pink_edges(target_dir)
|
||||||
@@ -1,48 +1,48 @@
|
|||||||
|
import cv2
|
||||||
from PIL import Image
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Configuration
|
# Define target widths for assets
|
||||||
# Configuration
|
TARGETS = {
|
||||||
TARGET_DIRS = ["assets/grounds", "assets/maps/tilesets"] # Expanded targets
|
'assets/DEMO_FAZA1/UI/badge_purchase.png': 512,
|
||||||
TARGET_SIZE = (256, 256)
|
'assets/DEMO_FAZA1/UI/health_critical.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/VFX/toxic_fog.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Environment/obstacle_thorns.png': 256,
|
||||||
|
'assets/DEMO_FAZA1/Structures/foundation_concrete.png': 512,
|
||||||
|
'assets/DEMO_FAZA1/Items/hay_drop_0.png': 128
|
||||||
|
}
|
||||||
|
|
||||||
def resize_images():
|
def resize_image(path, target_width):
|
||||||
for target_dir in TARGET_DIRS:
|
if not os.path.exists(path):
|
||||||
if not os.path.exists(target_dir):
|
print(f"Skipping {path}, file not found.")
|
||||||
print(f"⚠️ Directory {target_dir} not found. Skipping.")
|
return
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"📂 Processing directory: {target_dir}...")
|
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
||||||
|
if img is None:
|
||||||
files = [f for f in os.listdir(target_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
|
print(f"Error loading {path}")
|
||||||
|
return
|
||||||
for filename in files:
|
|
||||||
filepath = os.path.join(target_dir, filename)
|
h, w = img.shape[:2]
|
||||||
try:
|
|
||||||
with Image.open(filepath) as img:
|
# Check if resize is needed
|
||||||
width, height = img.size
|
if w <= target_width:
|
||||||
|
print(f"{path} is already {w}px (Target: {target_width}px). Skipping resize.")
|
||||||
# SELECTIVE RESIZE LOGIC
|
return
|
||||||
# 1. Must be Square 1024x1024 (Grounds, Props, Trees)
|
|
||||||
# 2. Or if explicitly in 'grounds' folder and > 256
|
# Calculate new height to maintain aspect ratio
|
||||||
|
ratio = target_width / w
|
||||||
should_resize = False
|
new_h = int(h * ratio)
|
||||||
|
|
||||||
if "grounds" in target_dir and width > 256:
|
resized = cv2.resize(img, (target_width, new_h), interpolation=cv2.INTER_AREA)
|
||||||
should_resize = True
|
|
||||||
elif width == 1024 and height == 1024:
|
cv2.imwrite(path, resized)
|
||||||
should_resize = True
|
print(f"Resized {path}: {w}x{h} -> {target_width}x{new_h}")
|
||||||
|
|
||||||
if should_resize:
|
|
||||||
print(f"✨ Resizing {filename} ({width}x{height}) -> {TARGET_SIZE} (Lanczos)")
|
|
||||||
img_resized = img.resize(TARGET_SIZE, Image.Resampling.LANCZOS)
|
|
||||||
img_resized.save(filepath)
|
|
||||||
else:
|
|
||||||
pass # print(f"Skipping {filename} ({width}x{height}) - Criteria not met")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error processing {filename}: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
resize_images()
|
base_path = os.getcwd()
|
||||||
|
print("Starting Resize Job...")
|
||||||
|
|
||||||
|
for rel_path, width in TARGETS.items():
|
||||||
|
full_path = os.path.join(base_path, rel_path)
|
||||||
|
resize_image(full_path, width)
|
||||||
|
|
||||||
|
print("Resize Job Complete.")
|
||||||
|
|||||||
75
scripts/utils/clean_and_move_image.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def clean_and_save(input_path, output_path):
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Processing: {input_path}")
|
||||||
|
|
||||||
|
# Read image
|
||||||
|
img = cv2.imread(input_path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Error: Could not read {input_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Convert to RGBA
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||||
|
|
||||||
|
# Convert to HSV for better color segmentation
|
||||||
|
hsv = cv2.cvtColor(img[:,:,:3], cv2.COLOR_BGR2HSV)
|
||||||
|
|
||||||
|
# Define Magenta range in HSV
|
||||||
|
# Magenta is around 300 degrees. OpenCV Hue is 0-179 (degrees/2). So 300/2 = 150.
|
||||||
|
# Range: 140 - 160 approx.
|
||||||
|
# We want to catch the solid pink background.
|
||||||
|
|
||||||
|
# Lower and Upper bounds for Magenta/Pink
|
||||||
|
# Hue: 140-170, Saturation: > 50, Value: > 50
|
||||||
|
lower_magenta = np.array([140, 50, 50])
|
||||||
|
upper_magenta = np.array([170, 255, 255])
|
||||||
|
|
||||||
|
mask = cv2.inRange(hsv, lower_magenta, upper_magenta)
|
||||||
|
|
||||||
|
# Invert mask (keep non-magenta)
|
||||||
|
mask_inv = cv2.bitwise_not(mask)
|
||||||
|
|
||||||
|
# Update alpha channel
|
||||||
|
# Where mask is 255 (magenta), alpha should be 0.
|
||||||
|
# We can just set the alpha channel to mask_inv directly.
|
||||||
|
# However, to avoid hard edges, we might want to erode/dilate, but for pixel/vector props hard cut is usually fine.
|
||||||
|
|
||||||
|
# Let's perform a small morphological operation to remove pink noise on edges if any
|
||||||
|
kernel = np.ones((1,1), np.uint8)
|
||||||
|
mask_inv = cv2.morphologyEx(mask_inv, cv2.MORPH_OPEN, kernel)
|
||||||
|
|
||||||
|
b, g, r, a = cv2.split(img)
|
||||||
|
img = cv2.merge((b, g, r, mask_inv))
|
||||||
|
|
||||||
|
# Crop to content (optional but good for assets)
|
||||||
|
# Find bounding box of non-transparent area
|
||||||
|
coords = cv2.findNonZero(mask_inv)
|
||||||
|
if coords is not None:
|
||||||
|
x, y, w, h = cv2.boundingRect(coords)
|
||||||
|
# Add 1px padding if possible
|
||||||
|
x = max(0, x-1)
|
||||||
|
y = max(0, y-1)
|
||||||
|
w = min(img.shape[1]-x, w+2)
|
||||||
|
h = min(img.shape[0]-y, h+2)
|
||||||
|
img = img[y:y+h, x:x+w]
|
||||||
|
|
||||||
|
# Save
|
||||||
|
cv2.imwrite(output_path, img)
|
||||||
|
print(f"Successfully saved cleaned image to {output_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description='Remove magenta background and save.')
|
||||||
|
parser.add_argument('input', help='Input file path')
|
||||||
|
parser.add_argument('output', help='Output file path')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
clean_and_save(args.input, args.output)
|
||||||
@@ -1,216 +1,289 @@
|
|||||||
export default class GrassScene extends Phaser.Scene {
|
export default class GrassSceneClean extends Phaser.Scene {
|
||||||
constructor() {
|
|
||||||
super({ key: 'GrassScene' });
|
|
||||||
this.baseTime = 12;
|
|
||||||
this.timeSpeed = 0.5;
|
|
||||||
this.playerSpeed = 200;
|
|
||||||
this.hayCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
console.log("🌿 Loading Clean Assets...");
|
this.load.path = 'assets/DEMO_FAZA1/';
|
||||||
|
|
||||||
const PATHS = {
|
// --- CHARACTERS ---
|
||||||
ground: 'assets/DEMO_FAZA1/Ground/',
|
this.load.image('kai', 'Characters/Kai_Dreads.png');
|
||||||
veg: 'assets/DEMO_FAZA1/Vegetation/',
|
|
||||||
char: 'assets/references/',
|
|
||||||
items: 'assets/DEMO_FAZA1/Items/Hay/',
|
|
||||||
audio: 'assets/audio/_NEW/'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.load.image('ground_base', PATHS.ground + 'tla_trava_tekstura.png');
|
// --- GROUND & ENVIRONMENT ---
|
||||||
this.load.image('grass_tall', PATHS.veg + 'visoka_trava.png');
|
this.load.image('ground_base', 'Ground/tla_trava_tekstura.png');
|
||||||
this.load.image('hay_drop', PATHS.items + 'hay_drop_0.png');
|
this.load.image('ground_dirt', 'Ground/ground_dirt_patch.png');
|
||||||
this.load.image('kai', PATHS.char + 'kaj.png');
|
|
||||||
this.load.audio('step_grass', PATHS.audio + 'footstep_grass_000.ogg');
|
|
||||||
|
|
||||||
this.load.on('loaderror', (file) => {
|
// --- WATER BIOMES ---
|
||||||
console.error('FAILED TO LOAD:', file.src);
|
// Layer 1 (Flat/Dirty)
|
||||||
});
|
this.load.image('puddle_mud', 'Environment/mud_puddle.png');
|
||||||
|
this.load.image('stream', 'Environment/stream_water.png');
|
||||||
|
|
||||||
|
// Layer 2 (Interactive/Structure)
|
||||||
|
this.load.image('water_clean', 'Environment/water_clean_patch.png');
|
||||||
|
|
||||||
|
// --- VEGETATION ---
|
||||||
|
this.load.image('grass_low', 'Vegetation/trava_sop.png');
|
||||||
|
this.load.image('grass_high', 'Vegetation/visoka_trava_v2.png');
|
||||||
|
this.load.image('grass_dense', 'Vegetation/grass_cluster_dense.png');
|
||||||
|
|
||||||
|
// --- OBSTACLES ---
|
||||||
|
this.load.image('thorns', 'Environment/obstacle_thorns.png');
|
||||||
|
this.load.image('hay', 'Items/hay_drop_0.png');
|
||||||
|
this.load.image('tree_big', 'Vegetation/drevo_veliko.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const { width, height } = this.scale;
|
// 1. WORLD SETUP
|
||||||
|
const WORLD_W = 2000;
|
||||||
|
const WORLD_H = 2000;
|
||||||
|
this.physics.world.setBounds(0, 0, WORLD_W, WORLD_H);
|
||||||
|
this.cameras.main.setBounds(0, 0, WORLD_W, WORLD_H);
|
||||||
|
this.cameras.main.setBackgroundColor('#1a3300');
|
||||||
|
|
||||||
// 1. BACKGROUND
|
// ZONES TRACKING (To prevent grass on water/objects)
|
||||||
this.ground = this.add.tileSprite(0, 0, width, height, 'ground_base')
|
// Array of {x, y, radius} or {rect}
|
||||||
.setOrigin(0, 0);
|
this.restrictedZones = [];
|
||||||
|
|
||||||
// 2. GROUPS
|
// --- LAYER 0: OSNOVA ---
|
||||||
this.tallGrassGroup = this.physics.add.group();
|
this.add.tileSprite(WORLD_W / 2, WORLD_H / 2, WORLD_W, WORLD_H, 'ground_base').setDepth(0);
|
||||||
this.hayGroup = this.physics.add.group();
|
|
||||||
|
|
||||||
this.allGrass = [];
|
// --- LAYER 1: VODA & UMAZANIJA (Flat, "vtopljeno" v tla) ---
|
||||||
|
// Trava NE SME rasti tukaj
|
||||||
|
this.createGroundLiquids(WORLD_W);
|
||||||
|
|
||||||
// 3. GENERATE WORLD - ONLY GRASS
|
// --- LAYER 2: INTERAKTIVNI OBJEKTI & Y-SORT GROUP ---
|
||||||
// "Vsa drevesa odstranjena. Samo trava ostane."
|
this.gameObjects = this.add.group({ runChildUpdate: true });
|
||||||
// "Nastavi neskončno travo čez celo mapo"
|
this.obstacles = this.physics.add.group(); // For thorns interaction
|
||||||
this.generateInfiniteGrass(width, height);
|
this.hayGroup = this.physics.add.group({ bounceX: 0.2, bounceY: 0.2, dragX: 100, dragY: 100 });
|
||||||
|
|
||||||
// 4. KAI (Player)
|
// Place Major Objects (Clean Water, Hay, Thorns, Big Trees)
|
||||||
this.player = this.physics.add.sprite(width / 2, height / 2, 'kai');
|
// with STRICT SPACING (100-150px)
|
||||||
this.player.setScale(0.5);
|
this.createMajorObjects(WORLD_W, WORLD_H);
|
||||||
this.player.setOrigin(0.5, 0.92);
|
|
||||||
this.player.setCollideWorldBounds(true);
|
|
||||||
this.player.setDepth(this.player.y);
|
|
||||||
|
|
||||||
this.lastStepTime = 0;
|
// --- VEGETATION FILL ---
|
||||||
|
// Fills the gaps, strictly respecting restrictedZones
|
||||||
|
this.populateVegetation(WORLD_W, WORLD_H);
|
||||||
|
|
||||||
// 5. INTERACTION LOGIC
|
// --- PLAYER (KAI) ---
|
||||||
this.physics.add.overlap(this.player, this.tallGrassGroup, this.handleMow, null, this);
|
this.kai = this.physics.add.sprite(1000, 1000, 'kai');
|
||||||
this.physics.add.overlap(this.player, this.hayGroup, this.collectHay, null, this);
|
this.kai.setCollideWorldBounds(true);
|
||||||
|
this.kai.body.setSize(30, 30);
|
||||||
|
this.kai.body.setOffset(this.kai.width / 2 - 15, this.kai.height - 30);
|
||||||
|
|
||||||
// Input
|
// Scale to 64px height strict
|
||||||
this.cursors = this.input.keyboard.createCursorKeys();
|
this.kai.setScale(64 / this.kai.height);
|
||||||
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
|
|
||||||
|
|
||||||
// 6. AMNESIA FADE IN
|
this.kai.setDepth(this.kai.y);
|
||||||
this.cameras.main.fadeIn(3000, 0, 0, 0);
|
this.gameObjects.add(this.kai);
|
||||||
|
|
||||||
// DYNAMIC VIGNETTE
|
// Physics Colliders for Layer 2 objects
|
||||||
const canvasTexture = this.textures.createCanvas('vignette_tex', width, height);
|
this.physics.add.collider(this.kai, this.hayGroup);
|
||||||
const ctx = canvasTexture.context;
|
this.physics.add.collider(this.hayGroup, this.hayGroup);
|
||||||
const grd = ctx.createRadialGradient(width / 2, height / 2, height * 0.3, width / 2, height / 2, height * 0.8);
|
|
||||||
grd.addColorStop(0, "rgba(0,0,0,0)");
|
|
||||||
grd.addColorStop(1, "rgba(0,0,0,0.9)");
|
|
||||||
|
|
||||||
ctx.fillStyle = grd;
|
// Camera
|
||||||
ctx.fillRect(0, 0, width, height);
|
this.cameras.main.startFollow(this.kai, true, 0.08, 0.08);
|
||||||
canvasTexture.refresh();
|
|
||||||
|
|
||||||
this.vignette = this.add.image(width / 2, height / 2, 'vignette_tex')
|
// AMNESIA VISUALS
|
||||||
.setScrollFactor(0)
|
if (this.cameras.main.postFX) {
|
||||||
.setAlpha(0)
|
const blur = this.cameras.main.postFX.addBlur(20, 2, 2, 1.0);
|
||||||
.setDepth(8000);
|
this.tweens.add({ targets: blur, strength: 0, duration: 5000, ease: 'Power2' });
|
||||||
|
|
||||||
// 7. DAY/NIGHT OVERLAY
|
|
||||||
this.dayNightOverlay = this.add.rectangle(0, 0, width, height, 0x000000, 0)
|
|
||||||
.setOrigin(0, 0)
|
|
||||||
.setDepth(9000);
|
|
||||||
|
|
||||||
// 8. UI INFO
|
|
||||||
this.infoText = this.add.text(20, 20, '', {
|
|
||||||
fontFamily: 'monospace', fontSize: '20px', fill: '#ffffff', stroke: '#000000', strokeThickness: 4
|
|
||||||
}).setDepth(10000);
|
|
||||||
|
|
||||||
this.inventoryText = this.add.text(width - 150, 20, 'Seno: 0', {
|
|
||||||
fontFamily: 'monospace', fontSize: '20px', fill: '#ffff00', stroke: '#000000', strokeThickness: 4
|
|
||||||
}).setDepth(10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateInfiniteGrass(w, h) {
|
|
||||||
// "Density = 1.0f" -> High density, cover everything.
|
|
||||||
// We'll use a grid loop with some randomization offset
|
|
||||||
// Grass scale is 0.2, original image is ~512?
|
|
||||||
// 0.2 * 512 = ~100px width.
|
|
||||||
// Step size ~50px for overlap.
|
|
||||||
|
|
||||||
const stepX = 40;
|
|
||||||
const stepY = 30;
|
|
||||||
|
|
||||||
for (let y = 0; y < h; y += stepY) {
|
|
||||||
for (let x = 0; x < w; x += stepX) {
|
|
||||||
// Add some randomness
|
|
||||||
let gx = x + Phaser.Math.Between(-10, 10);
|
|
||||||
let gy = y + Phaser.Math.Between(-10, 10);
|
|
||||||
|
|
||||||
// create(x, y, key)
|
|
||||||
let grass = this.tallGrassGroup.create(gx, gy, 'grass_tall');
|
|
||||||
grass.setScale(0.2);
|
|
||||||
grass.setOrigin(0.5, 0.95);
|
|
||||||
grass.setDepth(gy + 1);
|
|
||||||
|
|
||||||
grass.swaySpeed = 0.002 + Math.random() * 0.001;
|
|
||||||
grass.swayOffset = Math.random() * 100;
|
|
||||||
|
|
||||||
this.allGrass.push(grass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMow(player, grass) {
|
|
||||||
if (this.spaceKey.isDown) {
|
|
||||||
let hay = this.hayGroup.create(grass.x, grass.y + 10, 'hay_drop');
|
|
||||||
hay.setScale(0.6);
|
|
||||||
hay.setDepth(1);
|
|
||||||
hay.setAngle(Phaser.Math.Between(-20, 20));
|
|
||||||
grass.destroy();
|
|
||||||
const index = this.allGrass.indexOf(grass);
|
|
||||||
if (index > -1) this.allGrass.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectHay(player, hay) {
|
|
||||||
hay.destroy();
|
|
||||||
this.hayCount++;
|
|
||||||
this.inventoryText.setText(`Seno: ${this.hayCount}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(time, delta) {
|
|
||||||
// MOVEMENT LOGIC
|
|
||||||
let currentSpeed = this.playerSpeed;
|
|
||||||
let isMoving = false;
|
|
||||||
|
|
||||||
// HIDING CHECK (Always effectively in grass in this mode, but let's check overlap technically)
|
|
||||||
let inTallGrass = this.physics.overlap(this.player, this.tallGrassGroup);
|
|
||||||
|
|
||||||
if (inTallGrass) {
|
|
||||||
currentSpeed *= 0.6;
|
|
||||||
// "Player.CharacterAlpha = 0.4f"
|
|
||||||
this.player.setAlpha(0.4);
|
|
||||||
if (this.vignette.alpha < 1) this.vignette.alpha += 0.05;
|
|
||||||
} else {
|
} else {
|
||||||
// If he mows a clearing, he becomes visible
|
this.cameras.main.fadeIn(5000);
|
||||||
this.player.setAlpha(1.0);
|
|
||||||
if (this.vignette.alpha > 0) this.vignette.alpha -= 0.05;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player.setVelocity(0);
|
console.log("Nova Farma: Logical Layering Active.");
|
||||||
if (this.cursors.left.isDown) {
|
}
|
||||||
this.player.setVelocityX(-currentSpeed);
|
|
||||||
this.player.setFlipX(true);
|
createGroundLiquids(w) {
|
||||||
isMoving = true;
|
// Mud Puddles (Flat)
|
||||||
} else if (this.cursors.right.isDown) {
|
for (let i = 0; i < 8; i++) {
|
||||||
this.player.setVelocityX(currentSpeed);
|
let x = Phaser.Math.Between(200, 1800);
|
||||||
this.player.setFlipX(false);
|
let y = Phaser.Math.Between(200, 1800);
|
||||||
isMoving = true;
|
// Safe zone check
|
||||||
}
|
if (Phaser.Math.Distance.Between(x, y, 1000, 1000) < 300) continue;
|
||||||
if (this.cursors.up.isDown) {
|
|
||||||
this.player.setVelocityY(-currentSpeed);
|
let mud = this.add.image(x, y, 'puddle_mud');
|
||||||
isMoving = true;
|
mud.setDepth(0.01); // Layer 1
|
||||||
} else if (this.cursors.down.isDown) {
|
mud.setScale(1.2);
|
||||||
this.player.setVelocityY(currentSpeed);
|
|
||||||
isMoving = true;
|
// Register zone (exclude grass here)
|
||||||
|
// Radius approx width/2 * scale
|
||||||
|
this.restrictedZones.push({ x: x, y: y, radius: (mud.width / 2 * 1.2) - 10 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUDIO
|
// Stream (Flat)
|
||||||
if (isMoving && inTallGrass) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (time > this.lastStepTime + 350) {
|
let x = 300 + (i * 250);
|
||||||
this.sound.play('step_grass', { volume: 0.4 });
|
let y = 1600 + Math.sin(i) * 60;
|
||||||
this.lastStepTime = time;
|
let stream = this.add.image(x, y, 'stream');
|
||||||
|
stream.setDepth(0.01);
|
||||||
|
stream.setRotation(0.1);
|
||||||
|
|
||||||
|
this.restrictedZones.push({ x: x, y: y, radius: 100 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createMajorObjects(w, h) {
|
||||||
|
// We will maintain a list of major object positions to enforce the 150px grid spacing
|
||||||
|
// restrictedZones handles the water overlap, duplicate this list for object spacing if needed
|
||||||
|
// but adding to restrictedZones works for both.
|
||||||
|
|
||||||
|
// 1. Clean Water Panel (Interactive, Y-Sorted)
|
||||||
|
let wx = 1150, wy = 950;
|
||||||
|
let wClean = this.add.image(wx, wy, 'water_clean');
|
||||||
|
wClean.setOrigin(0.5, 0.9);
|
||||||
|
wClean.setScale(0.7); // Slightly smaller
|
||||||
|
this.restrictedZones.push({ x: wx, y: wy, radius: 80 });
|
||||||
|
this.gameObjects.add(wClean);
|
||||||
|
|
||||||
|
// 2. Thorns
|
||||||
|
// ... (Similar logic as before, ensure spacing)
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
let pos = this.findFreeSpot(w, h, 150);
|
||||||
|
if (pos) {
|
||||||
|
let thorn = this.obstacles.create(pos.x, pos.y, 'thorns');
|
||||||
|
thorn.setOrigin(0.5, 0.82);
|
||||||
|
thorn.body.setCircle(30);
|
||||||
|
thorn.body.setOffset(thorn.width / 2 - 30, thorn.height - 40);
|
||||||
|
thorn.setImmovable(true);
|
||||||
|
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 80 });
|
||||||
|
this.gameObjects.add(thorn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VISUALS
|
// 3. Hay
|
||||||
this.player.setDepth(this.player.y);
|
for (let i = 0; i < 6; i++) {
|
||||||
this.allGrass.forEach(g => { if (g.active) g.rotation = Math.sin((time * g.swaySpeed) + g.swayOffset) * 0.15; });
|
let pos = this.findFreeSpot(w, h, 100);
|
||||||
|
if (pos) {
|
||||||
|
let hay = this.hayGroup.create(pos.x, pos.y, 'hay');
|
||||||
|
hay.setOrigin(0.5, 0.85);
|
||||||
|
hay.setScale(0.8);
|
||||||
|
hay.body.setCircle(15);
|
||||||
|
hay.body.setOffset(hay.width / 2 - 15, hay.height - 20);
|
||||||
|
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 50 });
|
||||||
|
this.gameObjects.add(hay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.baseTime += (delta * 0.001 * this.timeSpeed);
|
// 4. BIG TREES
|
||||||
if (this.baseTime >= 24) this.baseTime = 0;
|
for (let i = 0; i < 10; i++) {
|
||||||
this.updateLighting(this.baseTime);
|
let pos = this.findFreeSpot(w, h, 200);
|
||||||
|
if (pos) {
|
||||||
let hour = Math.floor(this.baseTime);
|
let tree = this.add.image(pos.x, pos.y, 'tree_big');
|
||||||
let minute = Math.floor((this.baseTime % 1) * 60).toString().padStart(2, '0');
|
tree.setOrigin(0.5, 0.92);
|
||||||
this.infoText.setText(`Time: ${hour}:${minute}\n[Arrows] Premik\n[Space] Košnja`);
|
tree.setScale(1.3);
|
||||||
|
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 100 });
|
||||||
|
this.gameObjects.add(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLighting(hour) {
|
populateVegetation(w, h) {
|
||||||
let alpha = 0;
|
// Fill gaps with grass
|
||||||
let color = 0x000000;
|
// We use a looser spacing for grass (density) but strict check against restrictedZones
|
||||||
if (hour >= 20 || hour < 5) { alpha = 0.7; color = 0x000022; }
|
|
||||||
else if (hour >= 5 && hour < 8) { alpha = 0.3; color = 0xFF4500; }
|
for (let i = 0; i < 400; i++) {
|
||||||
else if (hour >= 18 && hour < 20) { alpha = 0.4; color = 0xFF4500; }
|
let x = Phaser.Math.Between(50, w - 50);
|
||||||
else { alpha = 0; }
|
let y = Phaser.Math.Between(50, h - 50);
|
||||||
this.dayNightOverlay.setFillStyle(color, alpha);
|
|
||||||
|
// Check strict zones (Water, Thorns, Trees)
|
||||||
|
if (this.isZoneRestricted(x, y)) continue;
|
||||||
|
|
||||||
|
let type = '';
|
||||||
|
let scale = 1.0;
|
||||||
|
let rng = Math.random();
|
||||||
|
|
||||||
|
if (rng > 0.9) {
|
||||||
|
// Dense Hiding Grass
|
||||||
|
type = 'grass_dense';
|
||||||
|
} else if (rng > 0.6) {
|
||||||
|
type = 'grass_high';
|
||||||
|
} else {
|
||||||
|
type = 'grass_low';
|
||||||
|
}
|
||||||
|
|
||||||
|
let grass = this.add.image(x, y, type);
|
||||||
|
grass.setOrigin(0.5, 1.0); // Pivot Bottom for sorting
|
||||||
|
|
||||||
|
// Scale logic: Knee to Waist height
|
||||||
|
// Kai ~64px. Grass ~30-45px.
|
||||||
|
let hTarget = 25 + Math.random() * 20;
|
||||||
|
grass.setScale(hTarget / grass.height);
|
||||||
|
|
||||||
|
// Tint blend
|
||||||
|
grass.setTint(0xcccccc);
|
||||||
|
|
||||||
|
this.gameObjects.add(grass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Find spot with minimal distance to others
|
||||||
|
findFreeSpot(w, h, minDistance) {
|
||||||
|
for (let i = 0; i < 50; i++) { // 50 attempts
|
||||||
|
let x = Phaser.Math.Between(100, w - 100);
|
||||||
|
let y = Phaser.Math.Between(100, h - 100);
|
||||||
|
|
||||||
|
// Safe spawn area check
|
||||||
|
if (Phaser.Math.Distance.Between(x, y, 1000, 1000) < 300) continue;
|
||||||
|
|
||||||
|
// Check specific restrictions
|
||||||
|
let ok = true;
|
||||||
|
for (let z of this.restrictedZones) {
|
||||||
|
if (Phaser.Math.Distance.Between(x, y, z.x, z.y) < (z.radius + minDistance)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) return { x, y };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isZoneRestricted(x, y) {
|
||||||
|
for (let z of this.restrictedZones) {
|
||||||
|
// For grass, we just need to be outside the radius of the object (water/tree)
|
||||||
|
if (Phaser.Math.Distance.Between(x, y, z.x, z.y) < z.radius) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
// 1. GLOBAL DEPTH & Y-SORT
|
||||||
|
this.gameObjects.children.each(child => {
|
||||||
|
child.setDepth(child.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. MOVEMENT & PHYSICS
|
||||||
|
const cursors = this.input.keyboard.createCursorKeys();
|
||||||
|
let speed = 200;
|
||||||
|
|
||||||
|
// Interaction Checks
|
||||||
|
let inThorns = false;
|
||||||
|
let inMud = false;
|
||||||
|
|
||||||
|
// Thorns
|
||||||
|
this.physics.overlap(this.kai, this.obstacles, () => inThorns = true);
|
||||||
|
|
||||||
|
// Mud (Sinking Effect)
|
||||||
|
this.physics.overlap(this.kai, this.mudGroup, () => inMud = true);
|
||||||
|
|
||||||
|
// Apply Effects
|
||||||
|
if (inThorns) {
|
||||||
|
speed = 100;
|
||||||
|
this.kai.setTint(0xffaaaa);
|
||||||
|
} else {
|
||||||
|
this.kai.clearTint();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inMud) {
|
||||||
|
this.kai.setAlpha(0.8); // "Ugrez" simulation
|
||||||
|
} else {
|
||||||
|
this.kai.setAlpha(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Velocity
|
||||||
|
this.kai.setVelocity(0);
|
||||||
|
if (cursors.left.isDown) this.kai.setVelocityX(-speed);
|
||||||
|
else if (cursors.right.isDown) this.kai.setVelocityX(speed);
|
||||||
|
if (cursors.up.isDown) this.kai.setVelocityY(-speed);
|
||||||
|
else if (cursors.down.isDown) this.kai.setVelocityY(speed);
|
||||||
|
|
||||||
|
this.kai.body.velocity.normalize().scale(speed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||