745 lines
31 KiB
Python
745 lines
31 KiB
Python
# =============================================================================
|
|
# Fishing Mini-Project - Main Game File (OUTLINE ONLY)
|
|
# =============================================================================
|
|
# This file is a STRUCTURAL OUTLINE: function signatures + descriptive comments.
|
|
# The actual implementation will be written by the project author (human).
|
|
#
|
|
# Engine: cmu_graphics | Pattern: MVC (Model-View-Controller)
|
|
# Data: creatures.tsv (tab-separated database at repo root)
|
|
# Save: save.json (persistent progress in the script directory)
|
|
# =============================================================================
|
|
|
|
from cmu_graphics import *
|
|
import json
|
|
import random
|
|
import math
|
|
import os
|
|
|
|
#debug vars to store mouse pos
|
|
gmouseX = 0
|
|
gmouseY = 0
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 1: DATA LOADING / FILE I/O
|
|
# -----------------------------------------------------------------------------
|
|
# Helpers that read from and write to disk. Kept separate from game logic so
|
|
# the data source (TSV path, save path) can change without touching gameplay.
|
|
# =============================================================================
|
|
|
|
def loadCreatures(path="creatures.tsv"):
|
|
# Read creatures.tsv (tab-separated) and parse it into a usable structure.
|
|
# Suggested return: a list of dicts, one per creature, with keys matching
|
|
# the header row: name, description, price, sprite-file, difficulty,
|
|
# length, weight. Convert numeric columns (price, difficulty, length,
|
|
# weight) from strings to int/float here; leave missing fields as sensible
|
|
# defaults so partially-filled rows (like the current TSV) don't crash.
|
|
creatures = []
|
|
with open(path) as f:
|
|
lines = f.readlines()
|
|
for line in lines[1:]: #ignore header
|
|
data = line.split("\t")
|
|
creatures.append({
|
|
"name": data[0],
|
|
"description": data[1],
|
|
"price": float(data[2]),
|
|
"sprite-file": data[3],
|
|
"difficulty": int(data[4]),
|
|
"length": float(data[5]), #inches
|
|
"weight": float(data[6]) #pounds
|
|
})
|
|
|
|
return creatures
|
|
|
|
|
|
def loadSave(path="save.json"):
|
|
# Load save.json and return the player's saved state (money, owned baits,
|
|
# owned upgrades, current bait, caught log, etc.). If the file is missing
|
|
# or unreadable, return a fresh default-state dict so a first-time player
|
|
# starts cleanly. This is the only place that reads the save file.
|
|
defaultState = {
|
|
"cash": 0,
|
|
"owned_baits": ["Worms"],
|
|
"owned_upgrades": [],
|
|
"current_bait": "Worms"
|
|
}
|
|
try:
|
|
with open(path, "r") as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, IOError):
|
|
return defaultState
|
|
|
|
|
|
def writeSave(saveState, path="save.json"):
|
|
# Serialize the current player state to save.json. Call this at key moments
|
|
# (after a sale, after a purchase, on quit) so progress persists between
|
|
# sessions. Keep the written structure identical to what loadSave expects.
|
|
with open(path, "w") as f: #overwrite existing
|
|
f.write(json.dumps(saveState))
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 2: MODEL - STATE SETUP & GAME DATA
|
|
# -----------------------------------------------------------------------------
|
|
# Everything that initializes and holds game state on the cmu_graphics `app`.
|
|
# No drawing and no event handling here - just state.
|
|
# =============================================================================
|
|
|
|
AVAILABLE_UPGRADES = [ # (name, bonus_type, bonus_modifier, price)
|
|
#power mods - lowers speed of cursor (additive)
|
|
("Smart Reel", "power", 2, 100),
|
|
("Clever Reel", "power", 7, 350),
|
|
("Intelligent Reel", "power", 10, 1500),
|
|
("Genius Reel", "power", 20, 20000),
|
|
#timing mods - lengthens bars in skill check (multiplicative)
|
|
("Medium Bobber", "timing", 1.1, 100),
|
|
("Large Bobber", "timing", 1.2, 300),
|
|
("Huge Bobber", "timing", 1.5, 1600),
|
|
("Massive Bobber", "timing", 2, 25000),
|
|
#count mods - lowers number of bars in skill check (subtractive (capped at 1))
|
|
("Shiny Rod", "count", 1, 450),
|
|
("Spectral Rod", "count", 1, 1200),
|
|
("Glowing Rod", "count", 2, 5000),
|
|
("Galaxy Rod", "count", 3, 40000)
|
|
]
|
|
|
|
AVAILABLE_BAITS = [ # (name, max_difficulty, price, image_file)
|
|
("Worms", 30, 0, "worm.png"),
|
|
("Prawns", 50, 200, "prawn.png"),
|
|
("Tuna", 70, 1000, "tuna.png"),
|
|
("Bass", 100, 19410, "bass.png")
|
|
|
|
]
|
|
|
|
def onAppStart(app):
|
|
# cmu_graphics entry point. Initialize ALL app.* state here:
|
|
# - window/layout constants (width, height, dock/water/grass regions)
|
|
# - app.creatures (from loadCreatures)
|
|
# - app.save (from loadSave) and derived player values
|
|
# - fishing state (idle / waiting-for-bite / skill-check / result)
|
|
# - skill-check state placeholders (cursor angle, bars, speed, result)
|
|
# - app.stepsPerSecond for the game loop cadence
|
|
# Delegates to the smaller setup helpers below to keep this readable.
|
|
|
|
app.debug = True
|
|
|
|
#DRAWING CONSTANTS
|
|
app.width = 600
|
|
app.height = 400
|
|
app.background = 'lightSkyBlue'
|
|
app.waterColor = 'midnightBlue'
|
|
app.grassColor = 'seaGreen'
|
|
app.waterHeight = app.height // 3
|
|
app.waterTop = app.height - app.waterHeight
|
|
app.grassHeight = 20
|
|
app.grassTop = app.height - app.waterHeight - app.grassHeight
|
|
|
|
#GAME STATE/SETUP
|
|
app.stepsPerSecond = 30
|
|
app.creatures = loadCreatures()
|
|
app.save = loadSave()
|
|
app.scene = "idle" # idle, waiting-for-bite, skill-check, result, shopping, menu
|
|
|
|
#SKILL CHECK
|
|
app.skillCheckAngle = 0
|
|
app.skillCheckSpeed = 1
|
|
app.skillCheckBars = [] # tuples of (position, length)
|
|
app.skillCheckFailed = False
|
|
app.showPatternTimer = 30
|
|
|
|
#FISHING
|
|
app.fishingTimer = 0
|
|
app.hookedFish = None
|
|
app.timeSinceInit = 0
|
|
|
|
#list of interactables with name and their positions
|
|
#(name, sceneFilter, left, right, top, bottom)
|
|
app.INTERACTABLES = [
|
|
("open-shop", "idle", 400, 540, app.grassTop - 130, app.grassTop + 30),
|
|
("close-shop", "shop", app.width-50, app.width, 0, 50)
|
|
]
|
|
|
|
#add all of our shop purchase upgrades
|
|
startX = 75
|
|
startY = 75
|
|
endX = 525
|
|
endY = 250
|
|
colInd = 0
|
|
XDiff = (endX-startX) // 3
|
|
|
|
#build upgrade grid
|
|
for columnType in ["power", "timing", "count"]:
|
|
upgrades = [u for u in AVAILABLE_UPGRADES if u[1] == columnType]
|
|
YDiff = (endY-startY) // len(upgrades)
|
|
for i in range(len(upgrades)):
|
|
app.INTERACTABLES.insert(0, ("upgrade-buy:" + upgrades[i][0],
|
|
"shop",
|
|
startX + XDiff * colInd,
|
|
startX + XDiff * colInd + XDiff,
|
|
startY + YDiff * i,
|
|
startY + YDiff * i + YDiff)) #this used to just be append
|
|
#but that was causing issues where the shop bounding box was
|
|
#checked first, then opening the shop gui, and allowing the
|
|
#player to purchase an upgrade with the same click (not ideal)
|
|
colInd += 1
|
|
|
|
#build bait grid
|
|
startX = 125
|
|
startY = 250
|
|
endX = 475
|
|
diffX = (endX - startX) // len(AVAILABLE_BAITS)
|
|
for i in range(len(AVAILABLE_BAITS)):
|
|
app.INTERACTABLES.insert(0, ("bait-buy:" + AVAILABLE_BAITS[i][0],
|
|
"shop",
|
|
startX + diffX * i,
|
|
startX + diffX * i + diffX,
|
|
startY,
|
|
startY + diffX))
|
|
|
|
print(app.INTERACTABLES)
|
|
|
|
|
|
def resetFishingState(app):
|
|
# Return the fishing cycle to its starting "idle" point so a new cast can
|
|
# begin. Clears any active skill-check data, the pending random bite timer,
|
|
# and the currently-hooked creature. Called at startup and after each catch
|
|
# is resolved/dismissed.
|
|
app.skillCheckAngle = 0 #measured in degrees
|
|
app.skillCheckSpeed = 1
|
|
app.skillCheckBars = [] # list of tuples of form (position, length)
|
|
app.skillCheckFailed = False
|
|
app.showPatternTimer = 30
|
|
|
|
app.hookedFish = None
|
|
app.fishingTimer = 0
|
|
|
|
app.scene = "idle"
|
|
pass
|
|
|
|
|
|
def pickRandomCreature(app):
|
|
# Choose which creature the player hooks on this cast. Selection is filtered
|
|
# by the current bait (bait gates which difficulty tiers are available) and
|
|
# weighted toward easier creatures, while still giving rarer/harder ones a
|
|
# real chance (per the spec's "skewed toward easier but still fun" goal).
|
|
# Returns the chosen creature record (one entry from app.creatures).
|
|
max_difficulty = [bait[1] for bait in AVAILABLE_BAITS if bait[0] == app.save["current_bait"]][0]
|
|
print("max difficulty:", max_difficulty)
|
|
|
|
#here we take the root of the difficulty to represent the weight of that fish, this makes higher difficulty fish easier to catch once unlocked
|
|
k = 0.75
|
|
#invert the difficulty and add one so it avoids zero
|
|
possibilities = [(creature["name"], (max_difficulty - creature["difficulty"] + 1) ** k) for creature in app.creatures if creature["difficulty"] <= max_difficulty]
|
|
print("possibilities:",possibilities)
|
|
total = sum([c[1] for c in possibilities])
|
|
print("total:",total)
|
|
|
|
#now pick randomly
|
|
roll = random.random() * (total) # a value between 0 and total
|
|
|
|
caughtName = "Aubrey Fish"
|
|
for possibleCatch in possibilities:
|
|
print("possible catch:",possibleCatch)
|
|
roll -= possibleCatch[1]
|
|
if (roll < 0):
|
|
caughtName = possibleCatch[0]
|
|
break
|
|
|
|
print(caughtName, app.creatures)
|
|
catch = [c for c in app.creatures if c["name"] == caughtName][0]
|
|
return catch
|
|
|
|
|
|
|
|
def startSkillCheck(app, creature):
|
|
# Configure a new skill check based on the hooked creature's difficulty.
|
|
# Difficulty maps to: cursor rotation speed, number of target bars, and
|
|
# bar length/width. Also apply any owned upgrades that modify these values
|
|
# (e.g. slower cursor, wider bars, extra forgiveness). Stores the resulting
|
|
# parameters on app for the controller/view to use.
|
|
app.scene = "skill-check"
|
|
difficulty = creature["difficulty"]
|
|
|
|
fishingPower = 1 + sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "power")])
|
|
|
|
app.skillCheckSpeed = max(difficulty / fishingPower, 5) #this got INCREDIBLY slow with good upgrades
|
|
|
|
numBars = difficulty // 15 - sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "count")])
|
|
numBars = max(numBars, 1)
|
|
|
|
lastPos = 0
|
|
lastLen = 0
|
|
while len(app.skillCheckBars) < numBars:
|
|
#50 is default length, adjust it proportionally to difficulty, then add upgrade length multipliers. also set to int instead of float
|
|
newLen = random.randrange(int((1 - (difficulty / 125)) * 60 * math.prod([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "timing")])))
|
|
#take the end of the last bar, and add a random amount between 5 degrees
|
|
#and the remaining amount to the end divided by the number of bars left
|
|
lastEndPos = (lastPos + lastLen)
|
|
newPos = lastEndPos + random.randrange(5, (360 - lastEndPos) // (numBars - len(app.skillCheckBars)))
|
|
#cap it to ensure we dont go over
|
|
newPos = min(newPos, 360-newLen)
|
|
#TODO: double check that multiple bars do not overlap
|
|
app.skillCheckBars.append((newPos, newLen))
|
|
lastPos = newPos
|
|
lastLen = newLen
|
|
|
|
#speed is set, bars are set, scene is good, should be fine?
|
|
|
|
def beginFishing(app):
|
|
app.fishingTimer = random.random() * app.stepsPerSecond * 25 + 5 # completely random between 5 and 30
|
|
app.scene = "waiting-for-bite"
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 3: CONTROLLER - GAME LOOP & INPUT HANDLING
|
|
# -----------------------------------------------------------------------------
|
|
# cmu_graphics event hooks. These advance state and route input; they should
|
|
# call Model helpers rather than contain heavy logic themselves.
|
|
# =============================================================================
|
|
|
|
def onStep(app):
|
|
# Runs every time step (the game loop / "controller tick").
|
|
# Responsibilities depend on the current fishing phase:
|
|
# - waiting-for-bite: count down the random timer; when it elapses, pick a
|
|
# creature and transition into the skill check.
|
|
# - skill-check: advance the cursor's angle around the circle each step.
|
|
# - otherwise (idle, viewing a catch, in shop): do nothing time-based.
|
|
|
|
app.timeSinceInit += 1
|
|
|
|
#had to look up how to do switch-case statement in python, I know it from JS though
|
|
match app.scene:
|
|
case "waiting-for-bite":
|
|
app.fishingTimer -= 1
|
|
if app.fishingTimer < 1:
|
|
selectedCreature = pickRandomCreature(app)
|
|
app.hookedFish = selectedCreature
|
|
startSkillCheck(app, selectedCreature)
|
|
case "skill-check":
|
|
if app.showPatternTimer < 1:
|
|
app.skillCheckAngle += app.skillCheckSpeed / 6 #this was far too fast
|
|
if (app.skillCheckAngle > 360):
|
|
finishSkillCheck(app)
|
|
else:
|
|
app.showPatternTimer -= 1
|
|
|
|
|
|
def onMousePress(app, mouseX, mouseY):
|
|
# Primary input router. Behavior depends on app.scene / fishing phase:
|
|
# - dock idle: clicking starts a cast (begin waiting-for-bite).
|
|
# - skill-check active: register the click as a timing attempt and
|
|
# evaluate it via checkSkillHit.
|
|
# - catch popup showing: dismiss the popup and reset fishing.
|
|
# - shop / UI sprites clicked: open the shop or trigger that UI action.
|
|
# Keep this as dispatch only - defer the real work to helper functions.
|
|
match app.scene:
|
|
case "idle" | "shop": #also had to look this up - pipe operator is dumb. just let me have multiple cases
|
|
interacted = False
|
|
#check if we clicked a hud element/interactable
|
|
for name, sceneFilter, left, right, top, bottom in app.INTERACTABLES:
|
|
if left < mouseX < right and top < mouseY < bottom \
|
|
and sceneFilter == app.scene:
|
|
interacted = True
|
|
match name:
|
|
case "open-shop":
|
|
app.scene = "shop"
|
|
case "close-shop":
|
|
app.scene = "idle"
|
|
if name.startswith("upgrade-buy:"):
|
|
print("buying upgrade", name)
|
|
buyUpgrade(app, name.split(":")[1])
|
|
if name.startswith("bait-buy:"):
|
|
print("buying bait", name)
|
|
buyBaitLicense(app, name.split(":")[1])
|
|
|
|
|
|
if not interacted and app.scene == "idle":
|
|
beginFishing(app)
|
|
case "result":
|
|
sellCreature(app, app.hookedFish)
|
|
resetFishingState(app)
|
|
case "skill-check":
|
|
checkSkillHit(app)
|
|
case "waiting-for-bite":
|
|
resetFishingState(app)
|
|
|
|
|
|
|
|
def onKeyPress(app, key):
|
|
# Optional keyboard controls. Candidates: a key to cast/reel as an
|
|
# alternative to clicking, a key to open/close the shop, or a debug/reset
|
|
# key during development. Remove if you decide the game is mouse-only.
|
|
|
|
#decidedly unused for now, i prefer mouse-only. ill keep the definition here in case i need debug tools.
|
|
|
|
# DEBUG
|
|
if not app.debug: return
|
|
if key == "m":
|
|
app.save["cash"] = 1000000
|
|
if key == "escape":
|
|
writeSave(app.save)
|
|
os._exit(0) #suggested by AI, instantly exits without hanging like sys.exit(0)
|
|
if key == "r":
|
|
#reset save
|
|
app.save = loadSave("thisisntarealsavefile.stupid")
|
|
if key == "f":
|
|
app.fishingTimer = 30
|
|
|
|
#added by AI, but simply allows me to trigger a breakpoint manually to check state of app and variables
|
|
#after some debate, this was the best option over something more complex
|
|
if key == 'd':
|
|
print("breakpoint here")
|
|
|
|
def onMouseMove(app, mouseX, mouseY):
|
|
global gmouseX, gmouseY
|
|
gmouseX, gmouseY = mouseX, mouseY
|
|
|
|
|
|
def checkSkillHit(app):
|
|
# Evaluate a skill-check click: is the cursor's current angle inside one of
|
|
# the active target bars? Decide hit vs miss. Define the multi-bar flow here
|
|
# (e.g. must the player clear every bar, or is one good hit enough?) and
|
|
# advance or fail the skill check accordingly. On full success, mark the
|
|
# creature as caught and move to the catch-display phase; on failure, end
|
|
# the cast without a catch.
|
|
try:
|
|
targetPos, targetLen = app.skillCheckBars.pop(0)
|
|
except IndexError:
|
|
targetPos = targetLen = -1 #make it impossible to succeed, this means they clicked after already getting rid of all bars
|
|
if not ( 0 < (app.skillCheckAngle - targetPos) < targetLen): #check if we hit a bar
|
|
#if we succeed, nothing needs to happen, we popped the bar
|
|
#if we fail, we either didnt hit or missed a past bar, either way we can jump straight to the fail screen
|
|
app.hookedFish = None
|
|
app.scene = "result"
|
|
print("failed",targetPos,targetLen,app.skillCheckAngle)
|
|
if len(app.skillCheckBars) < 1: #QOL: dont make them wait on an empty loop
|
|
finishSkillCheck(app)
|
|
|
|
|
|
def finishSkillCheck(app):
|
|
if len(app.skillCheckBars) > 0:
|
|
app.hookedFish = None #meaning we failed
|
|
app.scene = "result"
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 4: SHOP / ECONOMY LOGIC
|
|
# -----------------------------------------------------------------------------
|
|
# Buying/selling and the rogue-like upgrade system. Pure state changes that the
|
|
# controller invokes; no drawing here.
|
|
# =============================================================================
|
|
|
|
def sellCreature(app, creature):
|
|
# Add the creature's market value to the player's money. Value scales with
|
|
# difficulty (harder creatures pay more, per the spec). Persist the updated
|
|
# money via writeSave. Decide here whether selling is automatic on catch or
|
|
# a deliberate action in the shop/market.
|
|
|
|
#selling will be automatic
|
|
if creature == None: return
|
|
|
|
app.save["cash"] += creature["price"]
|
|
|
|
writeSave(app.save)
|
|
|
|
|
|
def buyUpgrade(app, upgradeId):
|
|
# Attempt to purchase an upgrade. Check the player can afford it and doesn't
|
|
# already own it; if valid, deduct money, record ownership in app.save, and
|
|
# persist. Upgrades modify the skill check (cursor speed, bar size/count,
|
|
# extra attempts, etc.) and are read back in startSkillCheck.
|
|
upgrade = [u for u in AVAILABLE_UPGRADES if u[0] == upgradeId][0] #find the specified upgrade
|
|
name, category, value, price = upgrade
|
|
print("attempting buy",upgrade)
|
|
|
|
if name in app.save["owned_upgrades"]: return False
|
|
if price > app.save["cash"]: return False
|
|
print("success!")
|
|
|
|
app.save["owned_upgrades"].append(name)
|
|
app.save["cash"] -= price
|
|
|
|
writeSave(app.save)
|
|
|
|
|
|
def buyBaitLicense(app, baitId):
|
|
# Purchase a one-time license for a bait tier. Once owned, the player can
|
|
# freely switch to that bait at no further cost (switching handled by
|
|
# setActiveBait). Bait determines which creatures are reachable in
|
|
# pickRandomCreature. Deduct money, record ownership, persist.
|
|
bait = [b for b in AVAILABLE_BAITS if b[0] == baitId][0] #find the specified upgrade
|
|
name, max_difficulty, price, img_file = bait
|
|
|
|
print("attempting buy",bait)
|
|
if name in app.save["owned_baits"]:
|
|
setActiveBait(app, name)
|
|
print("activated bait", name)
|
|
return False
|
|
if price > app.save["cash"]: return False
|
|
print("success!")
|
|
|
|
app.save["owned_baits"].append(name)
|
|
app.save["cash"] -= price
|
|
|
|
writeSave(app.save)
|
|
|
|
|
|
def setActiveBait(app, baitId):
|
|
# Switch the currently-equipped bait to any bait the player already owns a
|
|
# license for. Free action. Updates app state so the next cast's creature
|
|
# pool reflects the new bait. Reject the switch if the bait isn't owned.
|
|
bait = [b for b in AVAILABLE_BAITS if b[0] == baitId][0] #find the specified upgrade
|
|
name, max_difficulty, price, img_url = bait
|
|
|
|
if name in app.save["owned_baits"]:
|
|
app.save["current_bait"] = name
|
|
|
|
writeSave(app.save)
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 5: VIEW - DRAWING
|
|
# -----------------------------------------------------------------------------
|
|
# redrawAll + per-scene draw helpers. These ONLY read state and draw; they must
|
|
# never mutate app state. Composed top-down so redrawAll stays legible.
|
|
# =============================================================================
|
|
|
|
def redrawAll(app):
|
|
# Master draw routine. Draw the persistent world first (background, grass,
|
|
# water, dock, player), then layer the active scene/phase on top:
|
|
# - the shop UI, or
|
|
# - the active skill-check overlay, or
|
|
# - the catch-display popup.
|
|
# Delegates each piece to the helpers below.
|
|
drawBackground(app)
|
|
if app.scene == "waiting-for-bite":
|
|
drawBobber(app)
|
|
if app.scene == "shop":
|
|
drawShopEntities(app)
|
|
if app.scene == "skill-check":
|
|
drawSkillCheck(app)
|
|
if app.scene == "result":
|
|
drawCatchDisplay(app)
|
|
drawHud(app)
|
|
|
|
if app.debug:
|
|
drawDebug(app)
|
|
|
|
#my self-hosted gitea repo also stores the image files, very convienient
|
|
BASE_IMAGE_URL = "https://gitea.lockeshor.com/lockeshor/15112-fishing-miniproject/raw/branch/main/sprites/"
|
|
|
|
#REMINDERS:
|
|
# drawRect(left, top, width, height, fill="color")
|
|
# drawImage(url, )
|
|
# drawLabel(text, centerX, centerY)
|
|
def drawBackground(app):
|
|
# Draw the static scene layout from the spec: grass behind, the dock, and
|
|
# the lake occupying the bottom ~1/3 of the screen (blue water, optional
|
|
# wave patterns). This is the backdrop present in every scene.
|
|
|
|
#water
|
|
drawRect(0, app.waterTop, app.width, app.waterHeight, fill=app.waterColor)
|
|
#grass
|
|
drawRect(0, app.grassTop, app.width, app.grassHeight, fill=app.grassColor)
|
|
#dock
|
|
dockColor = 'sienna'
|
|
dockLegColor = 'maroon'
|
|
drawRect(0,app.grassTop - 50, 200, 40, fill = dockColor)
|
|
drawRect(40, app.grassTop - 10, 20, app.height, fill = dockLegColor)
|
|
drawRect(90, app.grassTop - 10, 20, app.height, fill = dockLegColor)
|
|
drawRect(140, app.grassTop - 10, 20, app.height, fill = dockLegColor)
|
|
|
|
#image cred: https://pngtree.com/freepng/cartoon-style-illustration-of-a-young-sleeping-men-fishing-on-a-wood-chair_15775817.html
|
|
#fisherman
|
|
drawImage(BASE_IMAGE_URL + "fisherman.png",
|
|
60, app.grassTop - 50 - 140, width=140, height=170)
|
|
|
|
#image cred: https://www.artstation.com/artwork/xzLbe2
|
|
#salesman
|
|
drawImage(BASE_IMAGE_URL + "salesman.png",
|
|
400, app.grassTop - 130, width = 140, height= 160)
|
|
|
|
#cash
|
|
drawLabel(f"${app.save["cash"]}",app.width - 5,5,align="right-top",size=26,fill='darkgoldenrod',font='orbitron')
|
|
|
|
|
|
def drawBobber(app):
|
|
# Draw the character sprite seated at the end of the dock with the fishing
|
|
# rod. When a cast is active, draw the line and the bobber dropping onto the
|
|
# water surface. Reflects the current fishing phase visually.
|
|
|
|
#cast line
|
|
drawLine(172, 64, 370, 280, fill="white")
|
|
|
|
#bobber?
|
|
|
|
|
|
def drawShopEntities(app):
|
|
# Draw the clickable shop/UI sprites standing on the grass (per the spec).
|
|
# Positions here must line up with the hit-testing done in onMousePress so
|
|
# clicks land on the right entity.
|
|
|
|
#dimming
|
|
drawRect(0,0,app.width,app.height,opacity=50)
|
|
#menu background
|
|
drawRect(50,50,app.width-100,app.height-100,fill='bisque',border='lightCoral',borderWidth=5)
|
|
#exit "X"
|
|
drawLine(app.width-45, 5, app.width-5, 45, fill='red', lineWidth=5)
|
|
drawLine(app.width-5, 5, app.width-45, 45, fill='red', lineWidth=5)
|
|
|
|
|
|
#draw upgrade purchases
|
|
for name, sceneFilter, left, right, top, bottom in [interactable for interactable in app.INTERACTABLES if interactable[0].startswith("upgrade-buy:")]:
|
|
#print("drawing",name,left,right,top,bottom)
|
|
displayName = name.split(":")[1]
|
|
#bg
|
|
drawRect(left, top, right - left, bottom - top, fill=('gray' if displayName in app.save["owned_upgrades"] else 'rosyBrown'), border='bisque',borderWidth=4)
|
|
|
|
centerY = top+(bottom-top)//2
|
|
#name
|
|
|
|
drawLabel(displayName, left+8, centerY, align="left",size=12)
|
|
|
|
#price
|
|
price = [u for u in AVAILABLE_UPGRADES if u[0] == displayName][0][3]
|
|
#pricebg
|
|
drawRect(right-8-40, top + 10, 40, bottom-top-20, fill="gold")
|
|
drawLabel("$" + str(price), right - 10, centerY, align="right", size = 10)
|
|
|
|
#draw bait purchases
|
|
for name, sceneFilter, left, right, top, bottom in [interactable for interactable in app.INTERACTABLES if interactable[0].startswith("bait-buy:")]:
|
|
displayName = name.split(":")[1]
|
|
|
|
drawRect(left, top, right - left, bottom - top, fill=('gray' if displayName in app.save["owned_baits"] else 'rosyBrown'), border=('green' if displayName == app.save["current_bait"] else'bisque'),borderWidth=4)
|
|
|
|
drawLabel(displayName, left + (right-left) / 2, bottom - 20, align="center")
|
|
|
|
img_file = [b for b in AVAILABLE_BAITS if b[0] == displayName][0][3]
|
|
|
|
|
|
#Image creds: ALL ICONS FROM https://www.flaticon.com
|
|
drawImage(BASE_IMAGE_URL + "bait-icons/" + img_file, left + (right-left) / 2, top + (bottom-top) / 2 - 10, width=50,height=50,align="center")
|
|
|
|
def drawSkillCheck(app):
|
|
# Draw the active skill-check overlay: the circle, the target bars placed
|
|
# around it (count/length from difficulty), and the rotating cursor at its
|
|
# current angle. Optionally show which bars are already cleared. Read-only.
|
|
|
|
#IMPORTANT: circle is like unit circle for drawing, but we want it to have 0 at the top. transformations will be needed
|
|
|
|
#radius of circle
|
|
r = 90
|
|
|
|
#basic path
|
|
#REMINDER: cx, cy, r
|
|
drawCircle(300, 130, r, fill=None,border='dimGray')
|
|
|
|
#bars
|
|
for barPos, barLen in app.skillCheckBars:
|
|
drawArc(300,130, r * 2 + 20, r * 2 + 20, -barPos + 90 - barLen, barLen, fill="white")
|
|
|
|
#cover up center of bars
|
|
drawCircle(300, 130, r - 10, fill=app.background)
|
|
|
|
#cursor
|
|
#REMINDER: width and height are for oval (so r * 2)
|
|
#REMINDER: cx, cy, width, height, startangle, sweepangle
|
|
drawArc(300,130, r * 2 + 20, r * 2 + 20, -app.skillCheckAngle + 90, 2, fill="red")
|
|
|
|
|
|
|
|
|
|
def drawCatchDisplay(app):
|
|
# Draw the "you caught it!" popup: the creature's sprite coming out of the
|
|
# water, plus its name, description, and stats (length, weight, value).
|
|
# Stays up until dismissed by a click (handled in onMousePress). Read-only.
|
|
|
|
#dimming
|
|
drawRect(0,0,app.width,app.height,opacity=75)
|
|
|
|
|
|
#star of surprise (turns out I cant make a star with assymetric height/width, so ill have to use an image)
|
|
#Image cred: https://pngtree.com/free-png-vectors/comics-bubbles
|
|
drawImage(BASE_IMAGE_URL + "surprise.png", app.width // 2, app.height // 2 - 40, align = "center",width = 530, height = 530)
|
|
|
|
|
|
#draw label
|
|
if app.hookedFish != None:
|
|
drawLabel(f"You Caught A {app.hookedFish["name"]}!",app.width // 2,210,align="center",size=18)
|
|
drawLabel(app.hookedFish["description"],app.width // 2,230,align="center",size=12)
|
|
#money bonus
|
|
drawRect(460, 180, 120, 45, align = "center", rotateAngle = -25, fill='mediumSpringGreen', border="black")
|
|
drawLabel(f"+ ${app.hookedFish["price"]}",460, 180, size=24, align = "center", rotateAngle = -25, fill='black')
|
|
|
|
#length and weight
|
|
drawLabel(f"{app.hookedFish["length"]} in.",app.width // 2 - 40,260,align="center",size=14)
|
|
drawLabel(f"{app.hookedFish["weight"]} lbs.",app.width // 2 + 40,260,align="center",size=14)
|
|
|
|
|
|
#draw creature/error
|
|
imgWidth = 200
|
|
if app.hookedFish != None:
|
|
trueimgW, trueimgH = getImageSize(BASE_IMAGE_URL + f"fish/{app.hookedFish["sprite-file"]}")
|
|
imgHeight = imgWidth * (trueimgH / trueimgW)
|
|
drawImage(BASE_IMAGE_URL + f"fish/{app.hookedFish["sprite-file"]}",
|
|
app.width // 2,
|
|
app.height // 2 - 75,
|
|
align = "center",
|
|
width=imgWidth,
|
|
height=imgHeight,
|
|
rotateAngle = math.sin(app.timeSinceInit / 20) * 10)
|
|
else:
|
|
#Image Cred: https://freepngimg.com/png/13474-fail-stamp-free-download-png
|
|
trueimgW, trueimgH = getImageSize(BASE_IMAGE_URL + "fail.png")
|
|
imgHeight = imgWidth * (trueimgH / trueimgW)
|
|
drawImage(BASE_IMAGE_URL + "fail.png",
|
|
app.width // 2,
|
|
app.height // 2,
|
|
align = "center",
|
|
width=imgWidth,
|
|
height=imgHeight,
|
|
rotateAngle = math.sin(app.timeSinceInit / 2) * 30 - 15)
|
|
|
|
|
|
|
|
def drawHud(app):
|
|
# Draw the persistent heads-up display: current money, equipped bait, and
|
|
# any brief status/instruction text ("click to cast", etc.). Overlaid on
|
|
# every scene so the player always sees their core stats.
|
|
|
|
#I dont really need this unless I decide to add alert/tutorial messages
|
|
#but i can draw the current bait in the corner
|
|
img_file = [b[3] for b in AVAILABLE_BAITS if b[0] == app.save["current_bait"]][0]
|
|
drawRect(app.width - 75, app.height - 75, 70, 70, fill='saddleBrown', border="black")
|
|
drawImage(BASE_IMAGE_URL + "bait-icons/" + img_file, app.width - 10, app.height - 10, width=60,height=60,align="bottom-right")
|
|
|
|
def drawDebug(app):
|
|
debugStr = f"""\
|
|
mX: {gmouseX}, mY: {gmouseY}
|
|
Scene: {app.scene}
|
|
fishingTimer: {rounded(app.fishingTimer)}
|
|
hookedFish: {app.hookedFish["name"] if app.hookedFish else "N/A"} @ {app.hookedFish["difficulty"] if app.hookedFish else "N/A"} difficulty
|
|
activeBait: {app.save["current_bait"]}
|
|
Angle: {rounded(app.skillCheckAngle)}
|
|
NextBar: {app.skillCheckBars[0] if len(app.skillCheckBars) > 0 else "N/A"}
|
|
showPatternTimer: {rounded(app.showPatternTimer)}
|
|
"""
|
|
debugLines = debugStr.splitlines()
|
|
size = 16
|
|
for i in range(len(debugLines)):
|
|
drawLabel(debugLines[i], 0, app.height - 125 + size * i, fill="white", size=size, align = "left")
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 6: PROGRAM ENTRY
|
|
# -----------------------------------------------------------------------------
|
|
def main():
|
|
# Start the cmu_graphics app (runApp with the chosen window dimensions).
|
|
runApp()
|
|
|
|
|
|
main()
|